package test

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

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

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

func newReferenceHandler(t *testing.T) *handler.ReferenceHandler {
	t.Helper()
	return handler.NewReferenceHandler(database.NewReferenceRepo(testutil.NewTestDB(t)))
}

// newReferenceServer spins up a real httptest.Server with the production
// mux routes. Use this for delete/path-param tests that need {id} routing.
func newReferenceServer(t *testing.T) *httptest.Server {
	t.Helper()
	h := newReferenceHandler(t)

	mux := http.NewServeMux()
	mux.HandleFunc("POST /api/v1/departments", h.CreateDepartment)
	mux.HandleFunc("GET /api/v1/departments", h.ListDepartments)
	mux.HandleFunc("DELETE /api/v1/departments/{id}", h.DeleteDepartment)
	mux.HandleFunc("POST /api/v1/gl-accounts", h.CreateGLAccount)
	mux.HandleFunc("GET /api/v1/gl-accounts", h.ListGLAccounts)
	mux.HandleFunc("DELETE /api/v1/gl-accounts/{id}", h.DeleteGLAccount)

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

// ── Department: Create ────────────────────────────────────────────────────────

func TestCreateDepartment_OK(t *testing.T) {
	h := newReferenceHandler(t)

	w := testutil.Do(t, http.HandlerFunc(h.CreateDepartment), http.MethodPost, "/",
		map[string]any{"code": "ENG", "name": "Engineering", "active": true})

	testutil.AssertStatus(t, w, http.StatusCreated)

	var got database.Department
	testutil.DecodeJSON(t, w, &got)
	if got.Code != "ENG" {
		t.Errorf("code: got %q, want %q", got.Code, "ENG")
	}
	if got.ID == 0 {
		t.Error("expected non-zero ID in response")
	}
	if !got.Active {
		t.Error("expected active=true")
	}
}

func TestCreateDepartment_DefaultsActiveTrue(t *testing.T) {
	h := newReferenceHandler(t)

	// active field omitted — handler must default it to true
	w := testutil.Do(t, http.HandlerFunc(h.CreateDepartment), http.MethodPost, "/",
		map[string]any{"code": "MKT", "name": "Marketing"})

	testutil.AssertStatus(t, w, http.StatusCreated)

	var got database.Department
	testutil.DecodeJSON(t, w, &got)
	if !got.Active {
		t.Error("expected active to default to true when omitted")
	}
}

func TestCreateDepartment_MissingCode(t *testing.T) {
	h := newReferenceHandler(t)
	w := testutil.Do(t, http.HandlerFunc(h.CreateDepartment), http.MethodPost, "/",
		map[string]any{"name": "Engineering"})
	testutil.AssertStatus(t, w, http.StatusBadRequest)
}

func TestCreateDepartment_MissingName(t *testing.T) {
	h := newReferenceHandler(t)
	w := testutil.Do(t, http.HandlerFunc(h.CreateDepartment), http.MethodPost, "/",
		map[string]any{"code": "ENG"})
	testutil.AssertStatus(t, w, http.StatusBadRequest)
}

func TestCreateDepartment_WhitespaceOnly(t *testing.T) {
	h := newReferenceHandler(t)
	w := testutil.Do(t, http.HandlerFunc(h.CreateDepartment), http.MethodPost, "/",
		map[string]any{"code": "   ", "name": "   "})
	testutil.AssertStatus(t, w, http.StatusBadRequest)
}

func TestCreateDepartment_InvalidJSON(t *testing.T) {
	h := newReferenceHandler(t)
	// nil body → empty body → JSON decode fails → 400
	w := testutil.Do(t, http.HandlerFunc(h.CreateDepartment), http.MethodPost, "/", nil)
	testutil.AssertStatus(t, w, http.StatusBadRequest)
}

func TestCreateDepartment_Upsert(t *testing.T) {
	h := newReferenceHandler(t)
	fn := http.HandlerFunc(h.CreateDepartment)

	testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "FIN", "name": "Finance"})

	w := testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "FIN", "name": "Finance Updated"})
	testutil.AssertStatus(t, w, http.StatusCreated)

	var got database.Department
	testutil.DecodeJSON(t, w, &got)
	if got.Name != "Finance Updated" {
		t.Errorf("upsert name: got %q, want %q", got.Name, "Finance Updated")
	}
}

// ── Department: List ──────────────────────────────────────────────────────────

func TestListDepartments_Empty(t *testing.T) {
	h := newReferenceHandler(t)

	w := testutil.Do(t, http.HandlerFunc(h.ListDepartments), http.MethodGet, "/", nil)
	testutil.AssertStatus(t, w, http.StatusOK)

	var got []database.Department
	testutil.DecodeJSON(t, w, &got)
	if len(got) != 0 {
		t.Errorf("expected empty list, got %d", len(got))
	}
}

func TestListDepartments_ReturnAll(t *testing.T) {
	h := newReferenceHandler(t)
	fn := http.HandlerFunc(h.CreateDepartment)

	testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "HR", "name": "Human Resources"})
	testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "IT", "name": "Information Technology"})
	testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "OPS", "name": "Operations"})

	w := testutil.Do(t, http.HandlerFunc(h.ListDepartments), http.MethodGet, "/", nil)
	testutil.AssertStatus(t, w, http.StatusOK)

	var got []database.Department
	testutil.DecodeJSON(t, w, &got)
	if len(got) != 3 {
		t.Errorf("expected 3 departments, got %d", len(got))
	}
}

// ── Department: Delete ────────────────────────────────────────────────────────

func TestDeleteDepartment_OK(t *testing.T) {
	srv := newReferenceServer(t)
	client := srv.Client()

	resp, err := client.Post(srv.URL+"/api/v1/departments", "application/json",
		mustJSON(t, map[string]any{"code": "OPS", "name": "Operations"}))
	if err != nil {
		t.Fatal(err)
	}
	var created database.Department
	json.NewDecoder(resp.Body).Decode(&created)
	resp.Body.Close()

	req, _ := http.NewRequest(http.MethodDelete, srv.URL+"/api/v1/departments/"+strconv.Itoa(created.ID), nil)
	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 TestDeleteDepartment_NotFound(t *testing.T) {
	srv := newReferenceServer(t)

	req, _ := http.NewRequest(http.MethodDelete, srv.URL+"/api/v1/departments/9999", nil)
	resp, err := srv.Client().Do(req)
	if err != nil {
		t.Fatal(err)
	}
	resp.Body.Close()

	// Accept 404 or 204 — adjust to match your handler's behaviour
	if resp.StatusCode != http.StatusNotFound && resp.StatusCode != http.StatusNoContent {
		t.Errorf("delete non-existent: got %d, want 404 or 204", resp.StatusCode)
	}
}

// ── GL Account: Create ────────────────────────────────────────────────────────

func TestCreateGLAccount_OK(t *testing.T) {
	h := newReferenceHandler(t)

	w := testutil.Do(t, http.HandlerFunc(h.CreateGLAccount), http.MethodPost, "/",
		map[string]any{"code": "5001", "name": "Travel Expenses", "type": "expense"})

	testutil.AssertStatus(t, w, http.StatusCreated)

	var got database.GLAccount
	testutil.DecodeJSON(t, w, &got)
	if got.Code != "5001" {
		t.Errorf("code: got %q, want %q", got.Code, "5001")
	}
	if got.ID == 0 {
		t.Error("expected non-zero ID")
	}
}

func TestCreateGLAccount_MissingCode(t *testing.T) {
	h := newReferenceHandler(t)
	w := testutil.Do(t, http.HandlerFunc(h.CreateGLAccount), http.MethodPost, "/",
		map[string]any{"name": "Travel Expenses"})
	testutil.AssertStatus(t, w, http.StatusBadRequest)
}

func TestCreateGLAccount_MissingName(t *testing.T) {
	h := newReferenceHandler(t)
	w := testutil.Do(t, http.HandlerFunc(h.CreateGLAccount), http.MethodPost, "/",
		map[string]any{"code": "5001"})
	testutil.AssertStatus(t, w, http.StatusBadRequest)
}

func TestCreateGLAccount_Upsert(t *testing.T) {
	h := newReferenceHandler(t)
	fn := http.HandlerFunc(h.CreateGLAccount)

	testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "4001", "name": "Revenue"})

	w := testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "4001", "name": "Revenue Updated"})
	testutil.AssertStatus(t, w, http.StatusCreated)

	var got model.GLAccount
	testutil.DecodeJSON(t, w, &got)
	if got.Name != "Revenue Updated" {
		t.Errorf("upsert name: got %q, want %q", got.Name, "Revenue Updated")
	}
}

// ── GL Account: List ──────────────────────────────────────────────────────────

func TestListGLAccounts_Empty(t *testing.T) {
	h := newReferenceHandler(t)
	w := testutil.Do(t, http.HandlerFunc(h.ListGLAccounts), http.MethodGet, "/", nil)
	testutil.AssertStatus(t, w, http.StatusOK)

	var got []database.GLAccount
	testutil.DecodeJSON(t, w, &got)
	if len(got) != 0 {
		t.Errorf("expected empty list, got %d", len(got))
	}
}

func TestListGLAccounts_ReturnAll(t *testing.T) {
	h := newReferenceHandler(t)
	fn := http.HandlerFunc(h.CreateGLAccount)

	testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "4001", "name": "Revenue"})
	testutil.Do(t, fn, http.MethodPost, "/", map[string]any{"code": "5001", "name": "COGS"})

	w := testutil.Do(t, http.HandlerFunc(h.ListGLAccounts), http.MethodGet, "/", nil)
	testutil.AssertStatus(t, w, http.StatusOK)

	var got []database.GLAccount
	testutil.DecodeJSON(t, w, &got)
	if len(got) != 2 {
		t.Errorf("expected 2 GL accounts, got %d", len(got))
	}
}

// ── GL Account: Delete ────────────────────────────────────────────────────────

func TestDeleteGLAccount_OK(t *testing.T) {
	srv := newReferenceServer(t)
	client := srv.Client()

	resp, err := client.Post(srv.URL+"/api/v1/gl-accounts", "application/json",
		mustJSON(t, map[string]any{"code": "6001", "name": "Rent"}))
	if err != nil {
		t.Fatal(err)
	}
	var created database.GLAccount
	json.NewDecoder(resp.Body).Decode(&created)
	resp.Body.Close()

	req, _ := http.NewRequest(http.MethodDelete, srv.URL+"/api/v1/gl-accounts/"+strconv.Itoa(created.ID), nil)
	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 TestDeleteGLAccount_NotFound(t *testing.T) {
	srv := newReferenceServer(t)

	req, _ := http.NewRequest(http.MethodDelete, srv.URL+"/api/v1/gl-accounts/9999", nil)
	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)
	}
}

// ── local helpers ─────────────────────────────────────────────────────────────

func mustJSON(t *testing.T, v any) *bytes.Reader {
	t.Helper()
	b, err := json.Marshal(v)
	if err != nil {
		t.Fatal(err)
	}
	return bytes.NewReader(b)
}
