package main

import (
	"fmt"
	"strconv"
	"testing"
	"time"

	"github.com/gavv/httpexpect/v2"
)

const (
	TestHistoryName = "Test History"
)

func startServer(t *testing.T) *httpexpect.Expect {
	config := ServerConfig{
		Datasource: ":memory:",
		//Datasource: "test.db",
		Addr:    ":0",
		Started: make(chan bool),
	}
	go start(t.Context(), &config)
	<-config.Started
	return httpexpect.Default(t, fmt.Sprintf("http://localhost:%d/api", config.Port))
}

func TestGetUsers(t *testing.T) {
	e := startServer(t)
	result := e.GET("/users").Expect().Status(200).JSON()
	result.Array().Length().IsEqual(2)
	result.Path("$[0].name").InList("Seeseemelk", "Huffle")
	result.Path("$[1].name").InList("Seeseemelk", "Huffle")
}

func TestGetUser(t *testing.T) {
	e := startServer(t)
	result := e.GET("/user/1").Expect().Status(200).JSON().Object()
	result.Value("name").IsEqual("Seeseemelk")
	result.Value("id").IsEqual(1)
	result.Value("allowance").IsEqual(0)
}

func TestGetUserUnknown(t *testing.T) {
	e := startServer(t)
	e.GET("/user/999").Expect().Status(404)
}

func TestGetUserBadId(t *testing.T) {
	e := startServer(t)
	e.GET("/user/bad-id").Expect().Status(400)
}

func TestGetUserAllowanceWhenNoAllowancePresent(t *testing.T) {
	e := startServer(t)
	result := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
	result.Length().IsEqual(1)
	item := result.Value(0).Object()
	item.Value("id").IsEqual(0)
}

func TestGetUserAllowance(t *testing.T) {
	e := startServer(t)

	// Create a new allowance
	requestBody := map[string]interface{}{
		"name":   TestHistoryName,
		"target": 5000,
		"weight": 10,
	}
	e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201)

	// Validate allowance
	result := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
	result.Length().IsEqual(2)
	item := result.Value(1).Object()
	item.Value("id").IsEqual(1)
	item.Value("name").IsEqual(TestHistoryName)
	item.Value("target").IsEqual(5000)
	item.Value("weight").IsEqual(10)
	item.Value("progress").IsEqual(0)
	item.NotContainsKey("user_id")
}

func TestGetUserAllowanceNoUser(t *testing.T) {
	e := startServer(t)
	e.GET("/user/999/allowance").Expect().Status(404)
}

func TestGetUserAllowanceBadId(t *testing.T) {
	e := startServer(t)
	e.GET("/user/bad-id/allowance").Expect().Status(400)
}

func TestCreateUserAllowance(t *testing.T) {
	e := startServer(t)

	// Create a new allowance
	requestBody := map[string]interface{}{
		"name":   TestHistoryName,
		"target": 5000,
		"weight": 10,
	}

	response := e.POST("/user/1/allowance").
		WithJSON(requestBody).
		Expect().
		Status(201).
		JSON().Object()

	// Verify the response has an ID
	response.ContainsKey("id")
	allowanceId := response.Value("id").Number().Raw()

	// Verify the allowance exists in the list of allowances
	allowances := e.GET("/user/1/allowance").
		Expect().
		Status(200).
		JSON().Array()

	allowances.Length().IsEqual(2)

	allowance := allowances.Value(1).Object()
	allowance.Value("id").IsEqual(allowanceId)
	allowance.Value("name").IsEqual(TestHistoryName)
	allowance.Value("target").IsEqual(5000)
	allowance.Value("weight").IsEqual(10)
	allowance.Value("progress").IsEqual(0)
}

func TestCreateUserAllowanceNoUser(t *testing.T) {
	e := startServer(t)

	requestBody := map[string]interface{}{
		"name":   TestHistoryName,
		"target": 5000,
		"weight": 10,
	}

	e.POST("/user/999/allowance").
		WithJSON(requestBody).
		Expect().
		Status(404)
}

func TestCreateUserAllowanceInvalidInput(t *testing.T) {
	e := startServer(t)

	// Test with empty name
	requestBody := map[string]interface{}{
		"name":   "",
		"target": 5000,
		"weight": 10,
	}

	e.POST("/user/1/allowance").
		WithJSON(requestBody).
		Expect().
		Status(400)

	// Test with missing fields
	invalidRequest := map[string]interface{}{
		"target": 5000,
	}

	e.POST("/user/1/allowance").
		WithJSON(invalidRequest).
		Expect().
		Status(400)
}

func TestCreateUserAllowanceBadId(t *testing.T) {
	e := startServer(t)

	requestBody := map[string]interface{}{
		"name":   TestHistoryName,
		"target": 5000,
		"weight": 10,
	}

	e.POST("/user/bad-id/allowance").
		WithJSON(requestBody).
		Expect().
		Status(400)
}

func TestDeleteUserAllowance(t *testing.T) {
	e := startServer(t)

	// Create a new allowance to delete
	createRequest := map[string]interface{}{
		"name":   TestHistoryName,
		"target": 1000,
		"weight": 5,
	}
	response := e.POST("/user/1/allowance").
		WithJSON(createRequest).
		Expect().
		Status(201).
		JSON().Object()

	allowanceId := response.Value("id").Number().Raw()

	// Delete the allowance
	e.DELETE("/user/1/allowance/" + strconv.Itoa(int(allowanceId))).
		Expect().
		Status(200).
		JSON().Object().Value("message").IsEqual("History deleted successfully")

	// Verify the allowance no longer exists
	allowances := e.GET("/user/1/allowance").
		Expect().
		Status(200).
		JSON().Array()
	allowances.Length().IsEqual(1)
}

func TestDeleteUserRestAllowance(t *testing.T) {
	e := startServer(t)
	e.DELETE("/user/1/allowance/0").Expect().Status(400)
}

func TestDeleteUserAllowanceNotFound(t *testing.T) {
	e := startServer(t)

	// Attempt to delete a non-existent allowance
	e.DELETE("/user/1/allowance/999").
		Expect().
		Status(404).
		JSON().Object().Value("error").IsEqual("History not found")
}

func TestDeleteUserAllowanceInvalidId(t *testing.T) {
	e := startServer(t)

	// Attempt to delete an allowance with an invalid ID
	e.DELETE("/user/1/allowance/invalid-id").
		Expect().
		Status(400).
		JSON().Object().Value("error").IsEqual("Invalid allowance ID")
}

func TestCreateTask(t *testing.T) {
	e := startServer(t)

	// Create a new task without assigned user
	requestBody := map[string]interface{}{
		"name":   "Test Task",
		"reward": 100,
	}

	response := e.POST("/tasks").
		WithJSON(requestBody).
		Expect().
		Status(201). // Expect Created status
		JSON().Object()

	// Verify the response has an ID
	response.ContainsKey("id")
	response.Value("id").Number().IsEqual(1)

	e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1)

	// Get task
	result := e.GET("/task/1").Expect().Status(200).JSON().Object()
	result.Value("id").IsEqual(1)
	result.Value("name").IsEqual("Test Task")
	result.Value("reward").IsEqual(100)
	result.Value("assigned").IsNull()

	// Create a new task with assigned user
	assignedUserId := 1
	requestBodyWithUser := map[string]interface{}{
		"name":     "Test Task Assigned",
		"reward":   200,
		"assigned": assignedUserId,
	}

	responseWithUser := e.POST("/tasks").
		WithJSON(requestBodyWithUser).
		Expect().
		Status(201).
		JSON().Object()

	responseWithUser.ContainsKey("id")
	responseWithUser.Value("id").Number().IsEqual(2)
}

func TestCreateScheduleTask(t *testing.T) {
	e := startServer(t)

	// Create a new task without assigned user
	requestBody := map[string]interface{}{
		"name":     "Test Task",
		"reward":   100,
		"schedule": "0 */5 * * * *",
	}

	response := e.POST("/tasks").
		WithJSON(requestBody).
		Expect().
		Status(201). // Expect Created status
		JSON().Object()

	requestBody["schedule"] = "every 5 seconds"
	e.POST("/tasks").WithJSON(requestBody).Expect().Status(400)

	// Verify the response has an ID
	response.ContainsKey("id")
	response.Value("id").Number().IsEqual(1)

	e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1)

	// Get task
	result := e.GET("/task/1").Expect().Status(200).JSON().Object()
	result.Value("id").IsEqual(1)
	result.Value("name").IsEqual("Test Task")
	result.Value("schedule").IsEqual("0 */5 * * * *")
	result.Value("reward").IsEqual(100)
	result.Value("assigned").IsNull()

	// Complete the task
	e.POST("/task/1/complete").Expect().Status(200)

	// Set expires date to 1 second in the past
	db.db.Query("update tasks set next_run = ? where id = 1").Bind(time.Now().Add(10 * -time.Minute).Unix()).MustExec()

	// Verify a new task is created
	newTask := e.GET("/task/2").Expect().Status(200).JSON().Object()
	newTask.Value("id").IsEqual(2)
	newTask.Value("name").IsEqual("Test Task")
	newTask.Value("schedule").IsEqual("0 */5 * * * *")
	newTask.Value("reward").IsEqual(100)
	newTask.Value("assigned").IsNull()
}

func TestDeleteTask(t *testing.T) {
	e := startServer(t)

	// Create a new task without assigned user
	requestBody := map[string]interface{}{
		"name":   "Test Task",
		"reward": 100,
	}

	response := e.POST("/tasks").
		WithJSON(requestBody).
		Expect().
		Status(201). // Expect Created status
		JSON().Object()

	// Verify the response has an ID
	response.ContainsKey("id")
	taskId := response.Value("id").Number().Raw()

	// Delete the task
	e.DELETE("/task/" + strconv.Itoa(int(taskId))).Expect().Status(200)
	// Verify the task no longer exists
	e.GET("/task/" + strconv.Itoa(int(taskId))).Expect().Status(404)
}

func TestDeleteTaskNotFound(t *testing.T) {
	e := startServer(t)
	e.DELETE("/task/1").Expect().Status(404)
}

func TestCreateTaskNoName(t *testing.T) {
	e := startServer(t)

	requestBody := map[string]interface{}{
		"reward": 100,
	}

	e.POST("/tasks").WithJSON(requestBody).Expect().Status(400)
}

func TestCreateTaskInvalidAssignedUser(t *testing.T) {
	e := startServer(t)

	requestBody := map[string]interface{}{
		"name":     "Test Task Invalid User",
		"reward":   100,
		"assigned": 999, // Non-existent user ID
	}

	e.POST("/tasks").
		WithJSON(requestBody).
		Expect().
		Status(404). // Expect Not Found
		JSON().Object().Value("error").IsEqual(ErrUserNotFound)
}

func TestCreateTaskInvalidRequestBody(t *testing.T) {
	e := startServer(t)

	// Test with missing fields (name is required)
	invalidRequest := map[string]interface{}{
		"reward": 5000,
	}

	e.POST("/tasks").
		WithJSON(invalidRequest).
		Expect().
		Status(400)
}

func TestGetTaskWhenNoTasks(t *testing.T) {
	e := startServer(t)

	result := e.GET("/tasks").Expect().Status(200).JSON().Array()
	result.Length().IsEqual(0)
}

func createTestTaskWithAmount(e *httpexpect.Expect, amount int) int {
	requestBody := map[string]interface{}{
		"name":   "Test Task",
		"reward": amount,
	}
	return int(e.POST("/tasks").WithJSON(requestBody).Expect().Status(201).JSON().Object().Value("id").Number().Raw())
}

func TestGetTasksWhenTasks(t *testing.T) {
	e := startServer(t)
	createTestTask(e)

	// Get the task
	result := e.GET("/tasks").Expect().Status(200).JSON().Array()
	result.Length().IsEqual(1)
	item := result.Value(0).Object()
	item.Value("id").IsEqual(1)
	item.Value("name").IsEqual("Test Task")
	item.Value("reward").IsEqual(100)
	item.Value("assigned").IsNull()
}

func TestGetTask(t *testing.T) {
	e := startServer(t)
	createTestTask(e)

	result := e.GET("/task/1").Expect().Status(200).JSON().Object()
	result.Value("id").IsEqual(1)
	result.Value("name").IsEqual("Test Task")
	result.Value("reward").IsEqual(100)
	result.Value("assigned").IsNull()
}

func TestGetTaskInvalidId(t *testing.T) {
	e := startServer(t)
	createTestTask(e)
	e.GET("/task/2").Expect().Status(404)
}

func TestGetTaskBadId(t *testing.T) {
	e := startServer(t)
	createTestTask(e)
	e.GET("/task/invalid").Expect().Status(400)
}

func TestPutTaskModifiesTask(t *testing.T) {
	e := startServer(t)
	createTestTask(e)
	requestBody := map[string]interface{}{
		"name":   "Updated Task",
		"reward": 100,
	}
	e.PUT("/task/1").WithJSON(requestBody).Expect().
		Status(200).
		JSON().Object()

	// Verify the task is updated
	result := e.GET("/task/1").Expect().Status(200).JSON().Object()
	result.Value("id").IsEqual(1)
	result.Value("name").IsEqual("Updated Task")
	result.Value("reward").IsEqual(100)
}

func TestPutTaskInvalidTaskId(t *testing.T) {
	e := startServer(t)
	createTestTask(e)

	requestBody := map[string]interface{}{
		"name": "Updated Task",
	}
	e.PUT("/task/999").WithJSON(requestBody).Expect().Status(404)
}

func TestPostHistory(t *testing.T) {
	e := startServer(t)

	e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 100, Description: "Add a 100"}).Expect().Status(200)
	e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 20, Description: "Lolol"}).Expect().Status(200)
	e.POST("/user/1/history").WithJSON(PostHistory{Allowance: -10, Description: "Subtracting"}).Expect().Status(200)

	response := e.GET("/user/1").Expect().Status(200).JSON().Object()
	response.Value("allowance").Number().IsEqual(100 + 20 - 10)
}

func TestPostHistoryInvalidUserId(t *testing.T) {
	e := startServer(t)

	e.POST("/user/999/history").WithJSON(PostHistory{Allowance: 100, Description: "Good"}).Expect().
		Status(404)
}

func TestPostHistoryInvalidDescription(t *testing.T) {
	e := startServer(t)

	e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 100}).Expect().
		Status(400)
}

func TestGetHistory(t *testing.T) {
	e := startServer(t)

	e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 100, Description: "Add 100"}).Expect().Status(200)
	e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 20, Description: "Add 20"}).Expect().Status(200)
	e.POST("/user/1/history").WithJSON(PostHistory{Allowance: -10, Description: "Subtract 10"}).Expect().Status(200)

	response := e.GET("/user/1/history").Expect().Status(200).JSON().Array()
	response.Length().IsEqual(3)
	response.Value(0).Object().Length().IsEqual(3)
	response.Value(0).Object().Value("allowance").Number().IsEqual(100)
	response.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
	response.Value(0).Object().Value("description").String().IsEqual("Add 100")

	response.Value(1).Object().Value("allowance").Number().IsEqual(20)
	response.Value(1).Object().Value("description").String().IsEqual("Add 20")

	response.Value(2).Object().Value("allowance").Number().IsEqual(-10)
	response.Value(2).Object().Value("description").String().IsEqual("Subtract 10")
}

func TestGetUserAllowanceById(t *testing.T) {
	e := startServer(t)

	// Create a new allowance
	requestBody := map[string]interface{}{
		"name":   TestHistoryName,
		"target": 5000,
		"weight": 10,
		"colour": "#FF5733",
	}
	resp := e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201).JSON().Object()
	allowanceId := int(resp.Value("id").Number().Raw())

	// Retrieve the created allowance by ID
	result := e.GET("/user/1/allowance/" + strconv.Itoa(allowanceId)).Expect().Status(200).JSON().Object()
	result.Value("id").IsEqual(allowanceId)
	result.Value("name").IsEqual(TestHistoryName)
	result.Value("target").IsEqual(5000)
	result.Value("weight").IsEqual(10)
	result.Value("progress").IsEqual(0)
	result.Value("colour").IsEqual("#FF5733")

	resultArray := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
	resultArray.Length().IsEqual(2)
	result = resultArray.Value(1).Object()
	result.Value("id").IsEqual(allowanceId)
	result.Value("name").IsEqual(TestHistoryName)
	result.Value("target").IsEqual(5000)
	result.Value("weight").IsEqual(10)
	result.Value("progress").IsEqual(0)
	result.Value("colour").IsEqual("#FF5733")
}

func TestGetUserByAllowanceIdInvalidAllowance(t *testing.T) {
	e := startServer(t)
	e.GET("/user/1/allowance/9999").Expect().Status(404)
}

func TestGetUserByAllowanceByIdInvalidUserId(t *testing.T) {
	e := startServer(t)
	e.GET("/user/999/allowance/1").Expect().Status(404)
}

func TestGetUserByAllowanceByIdBadUserId(t *testing.T) {
	e := startServer(t)
	e.GET("/user/bad/allowance/1").Expect().Status(400)
}

func TestGetUserByAllowanceByIdBadAllowanceId(t *testing.T) {
	e := startServer(t)
	e.GET("/user/1/allowance/bad").Expect().Status(400)
}

func TestPutAllowanceById(t *testing.T) {
	e := startServer(t)

	// Create a new allowance
	requestBody := map[string]interface{}{
		"name":   TestHistoryName,
		"target": 5000,
		"weight": 10,
		"colour": "#FF5733",
	}
	resp := e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201).JSON().Object()
	allowanceId := int(resp.Value("id").Number().Raw())

	// Update the allowance
	updateRequest := map[string]interface{}{
		"name":   "Updated Allowance",
		"target": 6000,
		"weight": 15,
		"colour": "#3357FF",
	}
	e.PUT("/user/1/allowance/" + strconv.Itoa(allowanceId)).WithJSON(updateRequest).Expect().Status(200)

	// Verify the allowance is updated
	result := e.GET("/user/1/allowance/" + strconv.Itoa(allowanceId)).Expect().Status(200).JSON().Object()
	result.Value("id").IsEqual(allowanceId)
	result.Value("name").IsEqual("Updated Allowance")
	result.Value("target").IsEqual(6000)
	result.Value("weight").IsEqual(15)
	result.Value("colour").IsEqual("#3357FF")
}

func TestCompleteTask(t *testing.T) {
	e := startServer(t)
	taskId := createTestTaskWithAmount(e, 101)

	e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1)

	// Update rest allowance
	e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{
		Weight: 25,
	}).Expect().Status(200)
	// Create two allowance goals
	e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{
		Name:   "Test Allowance 1",
		Target: 100,
		Weight: 50,
	}).Expect().Status(201)
	e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{
		Name:   "Test Allowance 1",
		Target: 10,
		Weight: 25,
	}).Expect().Status(201)

	// Complete the task
	e.POST("/task/" + strconv.Itoa(taskId) + "/complete").Expect().Status(200)

	// Verify the task is marked as completed
	e.GET("/task/" + strconv.Itoa(taskId)).Expect().Status(404)

	// Verify the allowances are updated for user 1
	allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
	allowances.Length().IsEqual(3)
	allowances.Value(0).Object().Value("id").Number().IsEqual(0)
	allowances.Value(0).Object().Value("progress").Number().InDelta(30.34, 0.01)
	allowances.Value(1).Object().Value("id").Number().IsEqual(1)
	allowances.Value(1).Object().Value("progress").Number().InDelta(60.66, 0.01)
	allowances.Value(2).Object().Value("id").Number().IsEqual(2)
	allowances.Value(2).Object().Value("progress").Number().IsEqual(10)

	// And also for user 2
	allowances = e.GET("/user/2/allowance").Expect().Status(200).JSON().Array()
	allowances.Length().IsEqual(1)
	allowances.Value(0).Object().Value("id").Number().IsEqual(0)
	allowances.Value(0).Object().Value("progress").Number().IsEqual(101)

	for userId := 1; userId <= 2; userId++ {
		userIdStr := strconv.Itoa(userId)
		// Ensure the history got updated
		history := e.GET("/user/" + userIdStr + "/history").Expect().Status(200).JSON().Array()
		history.Length().IsEqual(1)
		history.Value(0).Object().Value("allowance").Number().IsEqual(101)
		history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
	}
}

func TestCompleteTaskWithNoWeights(t *testing.T) {
	e := startServer(t)
	taskId := createTestTaskWithAmount(e, 101)

	e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1)

	// Ensure main allowance has no weight
	e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{
		Weight: 0,
	}).Expect().Status(200)

	// Complete the task
	e.POST("/task/" + strconv.Itoa(taskId) + "/complete").Expect().Status(200)

	// Verify the task is marked as completed
	e.GET("/task/" + strconv.Itoa(taskId)).Expect().Status(404)

	// Verify the allowances are updated for user 1
	allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
	allowances.Length().IsEqual(1)
	allowances.Value(0).Object().Value("id").Number().IsEqual(0)
	allowances.Value(0).Object().Value("progress").Number().InDelta(101.00, 0.01)

	// And also for user 2
	allowances = e.GET("/user/2/allowance").Expect().Status(200).JSON().Array()
	allowances.Length().IsEqual(1)
	allowances.Value(0).Object().Value("id").Number().IsEqual(0)
	allowances.Value(0).Object().Value("progress").Number().InDelta(101.00, 0.01)
}

func TestCompleteTaskAllowanceWeightsSumTo0(t *testing.T) {
	e := startServer(t)
	taskId := createTestTaskWithAmount(e, 101)

	e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1)

	// Update rest allowance
	e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{
		Weight: 0,
	}).Expect().Status(200)
	// Create an allowance goal
	createTestAllowance(e, "Test Allowance 1", 1000, 0)

	// Complete the task
	e.POST("/task/" + strconv.Itoa(taskId) + "/complete").Expect().Status(200)

	// Verify the task is marked as completed
	e.GET("/task/" + strconv.Itoa(taskId)).Expect().Status(404)

	// Verify the allowances are updated for user 1
	allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
	allowances.Length().IsEqual(2)
	allowances.Value(0).Object().Value("id").Number().IsEqual(0)
	allowances.Value(0).Object().Value("progress").Number().IsEqual(101)
	allowances.Value(1).Object().Value("id").Number().IsEqual(1)
	allowances.Value(1).Object().Value("progress").Number().IsEqual(0)
}

func TestCompleteTaskInvalidId(t *testing.T) {
	e := startServer(t)
	e.POST("/task/999/complete").Expect().Status(404)
}

func TestCompleteAllowance(t *testing.T) {
	e := startServer(t)
	createTestTaskWithAmount(e, 100)
	createTestAllowance(e, "Test Allowance 1", 100, 50)

	// Update base allowance
	e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{
		Weight: 0,
	}).Expect().Status(200)

	// Complete the task
	e.POST("/task/1/complete").Expect().Status(200)

	// Complete allowance goal
	e.POST("/user/1/allowance/1/complete").Expect().Status(200)

	// Verify the allowance no longer exists
	e.GET("/user/1/allowance/1").Expect().Status(404)

	// Verify history is updated
	history := e.GET("/user/1/history").Expect().Status(200).JSON().Array()
	history.Length().IsEqual(2)
	history.Value(0).Object().Length().IsEqual(3)
	history.Value(0).Object().Value("allowance").Number().IsEqual(100)
	history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
	history.Value(0).Object().Value("description").String().IsEqual("Task completed: Test Task")

	history.Value(1).Object().Length().IsEqual(3)
	history.Value(1).Object().Value("allowance").Number().IsEqual(-100)
	history.Value(1).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
	history.Value(1).Object().Value("description").String().IsEqual("Allowance completed: Test Allowance 1")
}

func TestCompleteAllowanceInvalidUserId(t *testing.T) {
	e := startServer(t)
	e.POST("/user/999/allowance/1/complete").Expect().Status(404)
}

func TestCompleteAllowanceInvalidAllowanceId(t *testing.T) {
	e := startServer(t)
	e.POST("/user/1/allowance/999/complete").Expect().Status(404)
}

func TestPutBulkAllowance(t *testing.T) {
	e := startServer(t)

	createTestAllowance(e, "Test Allowance 1", 1000, 1)
	createTestAllowance(e, "Test Allowance 2", 1000, 2)

	// Bulk edit
	request := []map[string]interface{}{
		{
			"id":     1,
			"weight": 5,
		},
		{
			"id":     0,
			"weight": 99,
		},
		{
			"id":     2,
			"weight": 10,
		},
	}
	e.PUT("/user/1/allowance").WithJSON(request).Expect().Status(200)

	// Verify the allowances are updated
	allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
	allowances.Length().IsEqual(3)
	allowances.Value(0).Object().Value("id").Number().IsEqual(0)
	allowances.Value(0).Object().Value("weight").Number().IsEqual(99)
	allowances.Value(1).Object().Value("id").Number().IsEqual(1)
	allowances.Value(1).Object().Value("weight").Number().IsEqual(5)
	allowances.Value(2).Object().Value("id").Number().IsEqual(2)
	allowances.Value(2).Object().Value("weight").Number().IsEqual(10)
}

func TestAddAllowanceSimple(t *testing.T) {
	e := startServer(t)

	createTestAllowance(e, "Test Allowance 1", 1000, 1)

	request := map[string]interface{}{
		"amount":      10,
		"description": "Added to allowance 1",
	}
	e.POST("/user/1/allowance/1/add").WithJSON(request).Expect().Status(200)

	// Verify the allowance is updated
	allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
	allowances.Value(1).Object().Value("id").Number().IsEqual(1)
	allowances.Value(1).Object().Value("progress").Number().InDelta(10.0, 0.01)

	// Verify the history is updated
	history := e.GET("/user/1/history").Expect().Status(200).JSON().Array()
	history.Length().IsEqual(1)
	history.Value(0).Object().Value("allowance").Number().InDelta(10.0, 0.01)
	history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
	history.Value(0).Object().Value("description").String().IsEqual("Added to allowance 1")
}

func TestAddAllowanceWithSpillage(t *testing.T) {
	e := startServer(t)

	createTestAllowance(e, "Test Allowance 1", 5, 1)
	createTestAllowance(e, "Test Allowance 2", 5, 1)
	e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{Weight: 1}).Expect().Status(200)

	request := map[string]interface{}{
		"amount":      10,
		"description": "Added to allowance 1",
	}
	e.POST("/user/1/allowance/1/add").WithJSON(request).Expect().Status(200)

	// Verify the allowance is updated
	allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
	allowances.Value(1).Object().Value("id").Number().IsEqual(1)
	allowances.Value(1).Object().Value("progress").Number().InDelta(5.0, 0.01)

	allowances.Value(2).Object().Value("id").Number().IsEqual(2)
	allowances.Value(2).Object().Value("progress").Number().InDelta(2.5, 0.01)

	allowances.Value(0).Object().Value("id").Number().IsEqual(0)
	allowances.Value(0).Object().Value("progress").Number().InDelta(2.5, 0.01)

	// Verify the history is updated
	history := e.GET("/user/1/history").Expect().Status(200).JSON().Array()
	history.Length().IsEqual(1)
	history.Value(0).Object().Value("allowance").Number().InDelta(10.0, 0.01)
	history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
	history.Value(0).Object().Value("description").String().IsEqual("Added to allowance 1")
}

func TestAddAllowanceIdZero(t *testing.T) {
	e := startServer(t)

	createTestAllowance(e, "Test Allowance 1", 1000, 1)

	request := map[string]interface{}{
		"amount":      10,
		"description": "Added to allowance 1",
	}
	e.POST("/user/1/allowance/0/add").WithJSON(request).Expect().Status(200)

	// Verify the allowance is updated
	allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
	allowances.Value(0).Object().Value("id").Number().IsEqual(0)
	allowances.Value(0).Object().Value("progress").Number().InDelta(10.0, 0.01)

	// Verify the history is updated
	history := e.GET("/user/1/history").Expect().Status(200).JSON().Array()
	history.Length().IsEqual(1)
	history.Value(0).Object().Value("allowance").Number().InDelta(10.0, 0.01)
	history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
	history.Value(0).Object().Value("description").String().IsEqual("Added to allowance 1")
}

func TestSubtractAllowanceSimple(t *testing.T) {
	e := startServer(t)

	createTestAllowance(e, "Test Allowance 1", 1000, 1)

	request := map[string]interface{}{
		"amount":      10,
		"description": "Added to allowance 1",
	}
	e.POST("/user/1/allowance/1/add").WithJSON(request).Expect().Status(200)
	request["amount"] = -2.5
	e.POST("/user/1/allowance/1/add").WithJSON(request).Expect().Status(200)

	// Verify the allowance is updated
	allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
	allowances.Value(1).Object().Value("id").Number().IsEqual(1)
	allowances.Value(1).Object().Value("progress").Number().InDelta(7.5, 0.01)

	// Verify the history is updated
	history := e.GET("/user/1/history").Expect().Status(200).JSON().Array()
	history.Length().IsEqual(2)
	history.Value(0).Object().Value("allowance").Number().InDelta(10.0, 0.01)
	history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
	history.Value(0).Object().Value("description").String().IsEqual("Added to allowance 1")

	history.Value(1).Object().Value("allowance").Number().InDelta(-2.5, 0.01)
	history.Value(1).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
	history.Value(1).Object().Value("description").String().IsEqual("Added to allowance 1")
}

func TestSubtractllowanceIdZero(t *testing.T) {
	e := startServer(t)

	createTestAllowance(e, "Test Allowance 1", 1000, 1)

	request := map[string]interface{}{
		"amount":      10,
		"description": "Added to allowance 1",
	}
	e.POST("/user/1/allowance/0/add").WithJSON(request).Expect().Status(200)
	request["amount"] = -2.5
	e.POST("/user/1/allowance/0/add").WithJSON(request).Expect().Status(200)

	// Verify the allowance is updated
	allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
	allowances.Value(0).Object().Value("id").Number().IsEqual(0)
	allowances.Value(0).Object().Value("progress").Number().InDelta(7.5, 0.01)

	// Verify the history is updated
	history := e.GET("/user/1/history").Expect().Status(200).JSON().Array()
	history.Length().IsEqual(2)
	history.Value(0).Object().Value("allowance").Number().InDelta(10.0, 0.01)
	history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
	history.Value(0).Object().Value("description").String().IsEqual("Added to allowance 1")

	history.Value(1).Object().Value("allowance").Number().InDelta(-2.5, 0.01)
	history.Value(1).Object().Value("description").String().IsEqual("Added to allowance 1")
}

func getDelta(base time.Time, delta float64) (time.Time, time.Time) {
	start := base.Add(-time.Duration(delta) * time.Second)
	end := base.Add(time.Duration(delta) * time.Second)
	return start, end
}

func createTestAllowance(e *httpexpect.Expect, name string, target float64, weight float64) {
	e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{
		Name:   name,
		Target: target,
		Weight: weight,
	}).Expect().Status(201)
}

func createTestTask(e *httpexpect.Expect) int {
	return createTestTaskWithAmount(e, 100)
}

// Transfer tests
func TestTransferSuccessful(t *testing.T) {
	e := startServer(t)

	// Create two allowances for user 1
	createTestAllowance(e, "From Allowance", 100, 1)
	createTestAllowance(e, "To Allowance", 100, 1)

	// Add 30 to allowance 1
	req := map[string]interface{}{"amount": 30, "description": "funds"}
	e.POST("/user/1/allowance/1/add").WithJSON(req).Expect().Status(200)

	// Transfer 10 from 1 to 2
	transfer := map[string]interface{}{"from": 1, "to": 2, "amount": 10}
	e.POST("/transfer").WithJSON(transfer).Expect().Status(200).JSON().Object().Value("message").IsEqual("Transfer successful")

	// Verify balances
	allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
	allowances.Value(1).Object().Value("progress").Number().InDelta(20.0, 0.01)
	allowances.Value(2).Object().Value("progress").Number().InDelta(10.0, 0.01)
}

func TestTransferCapsAtTarget(t *testing.T) {
	e := startServer(t)

	// Create two allowances
	createTestAllowance(e, "From Allowance", 100, 1)
	createTestAllowance(e, "To Allowance", 5, 1)

	// Add 10 to allowance 1
	req := map[string]interface{}{"amount": 10, "description": "funds"}
	e.POST("/user/1/allowance/1/add").WithJSON(req).Expect().Status(200)

	// Transfer 10 from 1 to 2, but to only needs 5
	transfer := map[string]interface{}{"from": 1, "to": 2, "amount": 10}
	e.POST("/transfer").WithJSON(transfer).Expect().Status(200)

	// Verify capped transfer
	allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
	allowances.Value(1).Object().Value("progress").Number().InDelta(5.0, 0.01) // from had 10, transferred 5 -> left 5
	allowances.Value(2).Object().Value("progress").Number().InDelta(5.0, 0.01) // to reached target
}

func TestTransferDifferentUsersFails(t *testing.T) {
	e := startServer(t)

	// Create allowance for user 1 and user 2
	createTestAllowance(e, "User1 Allowance", 100, 1)
	// create for user 2
	e.POST("/user/2/allowance").WithJSON(CreateAllowanceRequest{Name: "User2 Allowance", Target: 100, Weight: 1}).Expect().Status(201)

	// Add to user1 allowance
	req := map[string]interface{}{"amount": 10, "description": "funds"}
	e.POST("/user/1/allowance/1/add").WithJSON(req).Expect().Status(200)

	// Attempt transfer between different users
	transfer := map[string]interface{}{"from": 1, "to": 1 /* wrong id to simulate different user's id? */}
	// To ensure different user, fetch the allowance id for user2 (it's 1 for user2 in its own context but global id will be 2)
	// Create above for user2 produced global id 2, so use that
	transfer = map[string]interface{}{"from": 1, "to": 2, "amount": 5}
	e.POST("/transfer").WithJSON(transfer).Expect().Status(400)
}

func TestTransferInsufficientFunds(t *testing.T) {
	e := startServer(t)

	// Create two allowances
	createTestAllowance(e, "From Allowance", 100, 1)
	createTestAllowance(e, "To Allowance", 100, 1)

	// Ensure from has 0 balance
	transfer := map[string]interface{}{"from": 1, "to": 2, "amount": 10}
	resp := e.POST("/transfer").WithJSON(transfer).Expect().Status(400).JSON().Object()
	// Error text should mention insufficient funds
	resp.Value("error").String().Contains("insufficient")
}

func TestTransferNotFound(t *testing.T) {
	e := startServer(t)

	// No allowances exist yet (only user rows). Attempt transfer with non-existent IDs
	transfer := map[string]interface{}{"from": 999, "to": 1000, "amount": 1}
	e.POST("/transfer").WithJSON(transfer).Expect().Status(404)
}
