package test

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"Engine/internal/database"
	"Engine/internal/handler"
	"Engine/internal/model"
	"Engine/internal/service"
	"Engine/tests/internal/testutil"
)

// ── wire helpers ──────────────────────────────────────────────────────────────

func newBudgetServer(t *testing.T) (*httptest.Server, int, int) {
	t.Helper()
	db := testutil.NewTestDB(t)
	deptID, glID := testutil.SeedFixtures(t, db)

	repo := database.NewBudgetRepo(db)
	h := handler.NewBudgetHandler(service.NewBudgetService(repo))

	mux := http.NewServeMux()
	mux.HandleFunc("POST /api/v1/budget/create", h.Create)
	mux.HandleFunc("PUT /api/v1/budget/update", h.Update)
	mux.HandleFunc("DELETE /api/v1/budget/delete", h.Delete)

	srv := httptest.NewServer(mux)
	t.Cleanup(srv.Close)
	return srv, deptID, glID
}

func newBudgetHandler(t *testing.T) (*handler.BudgetHandler, int, int) {
	t.Helper()
	db := testutil.NewTestDB(t)
	deptID, glID := testutil.SeedFixtures(t, db)
	return handler.NewBudgetHandler(service.NewBudgetService(database.NewBudgetRepo(db))), deptID, glID
}

func validBudget(deptID, glID int) map[string]any {
	return map[string]any{
		"fiscal_year":   2024,
		"fiscal_period": 1,
		"version":       "original",
		"department_id": deptID,
		"gl_account_id": glID,
		"amount":        5000.00,
		"currency":      "DKK",
		"notes":         "",
		"created_by":    "test",
	}
}

// ── Create ────────────────────────────────────────────────────────────────────

func TestCreateBudget_OK(t *testing.T) {
	h, deptID, glID := newBudgetHandler(t)
	w := testutil.Do(t, http.HandlerFunc(h.Create), http.MethodPost, "/", validBudget(deptID, glID))
	testutil.AssertStatus(t, w, http.StatusCreated)

	var got model.Budget
	testutil.DecodeJSON(t, w, &got)
	if got.ID == 0 {
		t.Error("expected non-zero ID")
	}
	if got.Amount != 5000.00 {
		t.Errorf("amount: got %v, want 5000.00", got.Amount)
	}
}

func TestCreateBudget_InvalidJSON(t *testing.T) {
	h, _, _ := newBudgetHandler(t)
	w := testutil.Do(t, http.HandlerFunc(h.Create), http.MethodPost, "/", nil)
	testutil.AssertStatus(t, w, http.StatusBadRequest)
}

func TestCreateBudget_MissingPeriod(t *testing.T) {
	h, deptID, glID := newBudgetHandler(t)
	body := validBudget(deptID, glID)
	delete(body, "fiscal_period")
	w := testutil.Do(t, http.HandlerFunc(h.Create), http.MethodPost, "/", body)
	testutil.AssertStatus(t, w, http.StatusBadRequest)
}

func TestCreateBudget_NegativeAmount(t *testing.T) {
	h, deptID, glID := newBudgetHandler(t)
	body := validBudget(deptID, glID)
	body["amount"] = -100
	w := testutil.Do(t, http.HandlerFunc(h.Create), http.MethodPost, "/", body)
	testutil.AssertStatus(t, w, http.StatusBadRequest)
}

// ── Update ────────────────────────────────────────────────────────────────────

func TestUpdateBudget_OK(t *testing.T) {
	srv, deptID, glID := newBudgetServer(t)
	client := srv.Client()

	resp, err := client.Post(
		srv.URL+"/api/v1/budget/create",
		"application/json",
		mustJSON(t, validBudget(deptID, glID)),
	)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()

	var created model.Budget
	if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
		t.Fatalf("decode created budget: %v", err)
	}
	if created.ID == 0 {
		t.Fatal("expected a non-zero ID from create")
	}

	// Update only the fields the endpoint is meant to change
	wantAmount := 9999.99
	wantNotes := "updated in test"
	updateReq := model.UpdateBudgetRequest{
		ID:        created.ID,
		Amount:    &wantAmount,
		Notes:     &wantNotes,
		ChangedBy: created.CreatedBy,
	}

	req, err := http.NewRequest(
		http.MethodPut,
		srv.URL+"/api/v1/budget/update",
		mustJSON(t, updateReq),
	)
	if err != nil {
		t.Fatal(err)
	}
	req.Header.Set("Content-Type", "application/json")

	resp2, err := client.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp2.Body.Close()

	if resp2.StatusCode != http.StatusOK {
		t.Fatalf("update: got %d, want 200", resp2.StatusCode)
	}

	var got model.Budget
	if err := json.NewDecoder(resp2.Body).Decode(&got); err != nil {
		t.Fatalf("decode updated budget: %v", err)
	}

	if got.Amount != wantAmount {
		t.Errorf("amount: got %v, want %v", got.Amount, wantAmount)
	}
	if got.Notes != wantNotes {
		t.Errorf("notes: got %q, want %q", got.Notes, wantNotes)
	}
	if got.ID != created.ID {
		t.Errorf("ID changed after update: got %v, want %v", got.ID, created.ID)
	}

	// Fields not included in the update should be unchanged
	if got.FiscalYear != created.FiscalYear {
		t.Errorf("fiscal_year changed unexpectedly: got %v, want %v", got.FiscalYear, created.FiscalYear)
	}
	if got.DepartmentID != created.DepartmentID {
		t.Errorf("department_id changed unexpectedly: got %v, want %v", got.DepartmentID, created.DepartmentID)
	}
}

func TestUpdateBudget_BadRequest(t *testing.T) {
	srv, _, _ := newBudgetServer(t)

	// bad request...
	wantNotes := "updated in test"
	updateReq := model.UpdateBudgetRequest{
		Notes:     &wantNotes,
		ChangedBy: "idk",
	}

	req, err := http.NewRequest(
		http.MethodPut,
		srv.URL+"/api/v1/budget/update",
		mustJSON(t, updateReq),
	)
	resp, err := srv.Client().Do(req)
	if err != nil {
		t.Fatal(err)
	}
	resp.Body.Close()

	if resp.StatusCode != http.StatusBadRequest && resp.StatusCode != http.StatusNoContent {
		t.Errorf("update non-existent: got %d, want 400 or 204", resp.StatusCode)
	}
}

// ── Delete ────────────────────────────────────────────────────────────────────

func TestDeleteBudget_OK(t *testing.T) {
	srv, deptID, glID := newBudgetServer(t)
	client := srv.Client()

	resp, err := client.Post(
		srv.URL+"/api/v1/budget/create",
		"application/json",
		mustJSON(t, validBudget(deptID, glID)),
	)
	if err != nil {
		t.Fatal(err)
	}
	var created model.Budget
	json.NewDecoder(resp.Body).Decode(&created)
	resp.Body.Close()

	if created.ID == 0 {
		t.Fatal("expected non-zero ID from create")
	}

	req, err := http.NewRequest(
		http.MethodDelete,
		srv.URL+"/api/v1/budget/delete",
		mustJSON(t, model.DeleteBudgetRequest{ID: created.ID}),
	)
	if err != nil {
		t.Fatal(err)
	}
	req.Header.Set("Content-Type", "application/json")

	resp2, err := client.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	resp2.Body.Close()

	if resp2.StatusCode != http.StatusNoContent && resp2.StatusCode != http.StatusOK {
		t.Errorf("delete: got %d, want 200 or 204", resp2.StatusCode)
	}
}
func TestDeleteBudget_NotFound(t *testing.T) {
	srv, _, _ := newBudgetServer(t)

	req, _ := http.NewRequest(
		http.MethodDelete,
		srv.URL+"/api/v1/budget/delete",
		mustJSON(t, model.DeleteBudgetRequest{ID: 9999}),
	)
	req.Header.Set("Content-Type", "application/json")

	resp, err := srv.Client().Do(req)
	if err != nil {
		t.Fatal(err)
	}
	resp.Body.Close()

	if resp.StatusCode != http.StatusNotFound && resp.StatusCode != http.StatusNoContent {
		t.Errorf("delete non-existent: got %d, want 404 or 204", resp.StatusCode)
	}
}

func TestDeleteBudget_DoubleDelete(t *testing.T) {
	srv, deptID, glID := newBudgetServer(t)
	client := srv.Client()

	resp, err := client.Post(
		srv.URL+"/api/v1/budget/create",
		"application/json",
		mustJSON(t, validBudget(deptID, glID)),
	)
	if err != nil {
		t.Fatal(err)
	}
	var created model.Budget
	json.NewDecoder(resp.Body).Decode(&created)
	resp.Body.Close()

	if created.ID == 0 {
		t.Fatal("expected non-zero ID from create")
	}

	deleteBody := model.DeleteBudgetRequest{ID: created.ID}

	req1, _ := http.NewRequest(
		http.MethodDelete,
		srv.URL+"/api/v1/budget/delete",
		mustJSON(t, deleteBody),
	)
	req1.Header.Set("Content-Type", "application/json")
	resp1, _ := client.Do(req1)
	resp1.Body.Close()

	// Second delete — should not panic, should return 404 or 204
	req2, err := http.NewRequest(
		http.MethodDelete,
		srv.URL+"/api/v1/budget/delete",
		mustJSON(t, deleteBody),
	)
	if err != nil {
		t.Fatal(err)
	}
	req2.Header.Set("Content-Type", "application/json")

	resp2, err := client.Do(req2)
	if err != nil {
		t.Fatal(err)
	}
	resp2.Body.Close()

	if resp2.StatusCode != http.StatusNotFound && resp2.StatusCode != http.StatusNoContent {
		t.Errorf("double delete: got %d, want 404 or 204", resp2.StatusCode)
	}
}
