package finance

import (
	"bytes"
	"context"
	"encoding/json"
	"math"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/TaruLab/keluargamart-backend/internal/auth"
	"github.com/TaruLab/keluargamart-backend/internal/db"
	"github.com/go-chi/chi/v5"
	"github.com/jackc/pgx/v5/pgtype"
)

// platformCtx is a request context carrying a PLATFORM-WIDE store scope — what
// an admin/owner request actually has after auth.ScopeMiddleware runs.
//
// It exists because the cashflow surface is fail-closed: since migration 000104
// a request with NO scope attached is refused rather than treated as
// unrestricted, so a test that used a bare context.Background() was relying on
// the old permissive default without meaning to. Attaching the scope explicitly
// says what the test means — "a caller who may see everything" — and keeps the
// fail-closed behaviour honest for the tests that are ABOUT it.
func platformCtx() context.Context {
	return auth.WithStoreScope(context.Background(), auth.NewStoreScope(nil))
}

// outletCtx is the same, for a caller restricted to the given stores.
//
// THE EMPTY CASE IS NOT A VARIADIC ONE. A variadic parameter called with no
// arguments is a NIL slice, not an empty one, and nil means PLATFORM-WIDE — so
// outletCtx() written as `ids ...int64` would have handed back a scope that sees
// the entire company while claiming to see nothing. That is the same fail-open
// mistake this lane exists to prevent, so the slice is built explicitly and the
// nil case is excluded by construction.
//
// Two tests depend on this: RestrictedWithNoStoresSeesNothing and
// FilterReachesTheStore/restricted-to-nothing.
func outletCtx(ids ...int64) context.Context {
	out := make([]int64, 0, len(ids))
	out = append(out, ids...)
	return auth.WithStoreScope(context.Background(), auth.NewStoreScope(out))
}

func TestHandlerDeleteContracts(t *testing.T) {
	s := svc()
	st := s.store.(*fakeStore)
	st.cashflows[1] = &Cashflow{ID: 1}
	r := chi.NewRouter()
	r.Delete("/cashflows/{id}", NewHandler(s).DeleteCashflow)
	req := httptest.NewRequest(http.MethodDelete, "/cashflows/1", nil).WithContext(platformCtx())
	res := httptest.NewRecorder()
	r.ServeHTTP(res, req)
	if res.Code != http.StatusNoContent || res.Body.Len() != 0 {
		t.Fatalf("cashflow delete = status %d body %q, want 204 with empty body", res.Code, res.Body.String())
	}

	st.invoices[2] = &Invoice{ID: 2, Status: "unpaid", SalesOrderID: 3}
	st.sos[3] = &SOHeader{ID: 3, Status: "confirmed"}
	st.soStatusHistory = append(st.soStatusHistory, dbHistoryForInvoice(3, 2))
	r = chi.NewRouter()
	r.Delete("/invoices/{id}", NewHandler(s).DeleteInvoice)
	req = httptest.NewRequest(http.MethodDelete, "/invoices/2", nil)
	res = httptest.NewRecorder()
	r.ServeHTTP(res, req)
	if res.Code != http.StatusNoContent || res.Body.Len() != 0 {
		t.Fatalf("invoice delete = status %d body %q, want 204 with empty body", res.Code, res.Body.String())
	}
}

func dbHistoryForInvoice(soID, invoiceID int64) db.SalesOrderStatusHistory {
	return db.SalesOrderStatusHistory{SalesOrderID: soID, FromStatus: "confirmed", ToStatus: "invoiced", Reason: "invoice_created", ReferenceType: textOrNil(InvoiceModel), ReferenceID: pgtype.Int8{Int64: invoiceID, Valid: true}}
}

func TestHandlerCollectionEnvelopeContracts(t *testing.T) {
	h := NewHandler(svc())
	for name, handler := range map[string]http.HandlerFunc{
		"cashflows":   h.ListCashflows,
		"receivables": h.ListReceivables,
	} {
		res := httptest.NewRecorder()
		// platformCtx: the cashflow surface is fail-closed on an absent scope,
		// so this has to say what it means. See platformCtx.
		req := httptest.NewRequest(http.MethodGet, "/?page=1&per_page=20", nil).WithContext(platformCtx())
		handler(res, req)
		assertCollectionEnvelope(t, name, res)
	}
}

func assertCollectionEnvelope(t *testing.T, name string, res *httptest.ResponseRecorder) {
	t.Helper()
	if res.Code != http.StatusOK {
		t.Fatalf("%s status = %d, want 200", name, res.Code)
	}
	var body struct {
		Data []json.RawMessage `json:"data"`
		Meta struct {
			Pagination json.RawMessage `json:"pagination"`
		} `json:"meta"`
	}
	if err := json.NewDecoder(strings.NewReader(res.Body.String())).Decode(&body); err != nil {
		t.Fatalf("%s invalid JSON: %v", name, err)
	}
	if body.Data == nil || len(body.Meta.Pagination) == 0 || string(body.Meta.Pagination) == "null" {
		t.Fatalf("%s envelope = %s, want data array and meta.pagination", name, res.Body.String())
	}
}

func TestHandlerPayInvoice_ReferenceNumberRequired(t *testing.T) {
	s := svc()
	r := chi.NewRouter()
	r.Post("/invoices/{id}/pay", NewHandler(s).PayInvoice)

	// Case 1: missing reference_number -> 422
	reqBody := `{"amount": 100000, "payment_method": "transfer"}`
	req := httptest.NewRequest(http.MethodPost, "/invoices/1/pay", strings.NewReader(reqBody))
	req.Header.Set("Content-Type", "application/json")
	res := httptest.NewRecorder()
	r.ServeHTTP(res, req)
	if res.Code != http.StatusUnprocessableEntity {
		t.Fatalf("pay invoice without reference = status %d, want 422", res.Code)
	}

	// Case 2: empty reference_number -> 422
	reqBodyEmpty := `{"amount": 100000, "payment_method": "transfer", "reference_number": "   "}`
	req = httptest.NewRequest(http.MethodPost, "/invoices/1/pay", strings.NewReader(reqBodyEmpty))
	req.Header.Set("Content-Type", "application/json")
	res = httptest.NewRecorder()
	r.ServeHTTP(res, req)
	if res.Code != http.StatusUnprocessableEntity {
		t.Fatalf("pay invoice with whitespace reference = status %d, want 422", res.Code)
	}
}

func verifyRouter(s *Service) http.Handler {
	r := chi.NewRouter()
	r.Post("/payments/{id}/verify", NewHandler(s).VerifyPayment)
	return r
}

// A verify/reject is an audited review, not a status flip: the payments table
// has no status column, so the ONLY truthful record is the verdict note. This
// asserts the note that actually reached the store carries the reviewer
// identity taken from the request context, for BOTH directions, and that the
// response reports the persisted verdict instead of a fabricated status.
func TestHandlerVerifyPayment_PersistsVerdictWithReviewer(t *testing.T) {
	cases := []struct {
		name        string
		approved    bool
		user        *auth.User
		wantVerdict string  // payments.notes value
		wantState   string  // response "verdict" field
		wantStatus  string  // response compatibility alias "status"
		wantBy      float64 // response "reviewed_by"; NaN = field absent
	}{
		{
			name:     "approve stamps verified with reviewer id and name",
			approved: true, user: &auth.User{ID: 9, Name: "Budi"},
			wantVerdict: "[Verified] pembayaran diverifikasi oleh #9 (Budi)",
			wantState:   PaymentVerdictVerified, wantStatus: "paid", wantBy: 9,
		},
		{
			name:     "reject stamps rejected with reviewer id and name",
			approved: false, user: &auth.User{ID: 42, Name: "Sinta"},
			wantVerdict: "[Rejected] pembayaran ditolak oleh #42 (Sinta)",
			wantState:   PaymentVerdictRejected, wantStatus: "failed", wantBy: 42,
		},
		{
			name:     "reviewer without a name still records the id",
			approved: true, user: &auth.User{ID: 5},
			wantVerdict: "[Verified] pembayaran diverifikasi oleh #5",
			wantState:   PaymentVerdictVerified, wantStatus: "paid", wantBy: 5,
		},
		{
			name:     "anonymous review omits identity rather than inventing one",
			approved: false, user: nil,
			wantVerdict: "[Rejected] pembayaran ditolak",
			wantState:   PaymentVerdictRejected, wantStatus: "failed", wantBy: math.NaN(),
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			s := svc()
			st := s.store.(*fakeStore)
			st.payments[1] = &Payment{ID: 1, PaymentNumber: "PAY-1", PayableType: InvoiceModel, PayableID: 3}

			body, _ := json.Marshal(map[string]bool{"approved": tc.approved})
			req := httptest.NewRequest(http.MethodPost, "/payments/1/verify", bytes.NewReader(body))
			req.Header.Set("Content-Type", "application/json")
			ctx := platformCtx()
			if tc.user != nil {
				ctx = auth.ToContext(ctx, tc.user, 1)
			}
			req = req.WithContext(ctx)
			res := httptest.NewRecorder()
			verifyRouter(s).ServeHTTP(res, req)
			if res.Code != http.StatusOK {
				t.Fatalf("verify = status %d body %s, want 200", res.Code, res.Body.String())
			}

			// The PERSISTED note is the assertion: returning the right string
			// from the service while the store got a different one is the bug.
			if got := st.paymentReviews[1]; got != tc.wantVerdict {
				t.Fatalf("persisted notes = %q, want %q", got, tc.wantVerdict)
			}

			var envelope struct {
				Data struct {
					ID         int64    `json:"id"`
					Status     string   `json:"status"`
					Verdict    string   `json:"verdict"`
					Notes      string   `json:"notes"`
					ReviewedBy *float64 `json:"reviewed_by"`
				} `json:"data"`
			}
			if err := json.Unmarshal(res.Body.Bytes(), &envelope); err != nil {
				t.Fatalf("verify response invalid JSON: %v", err)
			}
			out := envelope.Data
			if out.ID != 1 {
				t.Errorf("id = %d, want 1", out.ID)
			}
			if out.Notes != tc.wantVerdict {
				t.Errorf("response notes = %q, want the persisted %q", out.Notes, tc.wantVerdict)
			}
			if out.Verdict != tc.wantState {
				t.Errorf("verdict = %q, want %q", out.Verdict, tc.wantState)
			}
			if out.Status != tc.wantStatus {
				t.Errorf("status alias = %q, want %q (backward compatibility)", out.Status, tc.wantStatus)
			}
			switch {
			case math.IsNaN(tc.wantBy):
				if out.ReviewedBy != nil {
					t.Errorf("reviewed_by = %v, want absent for an anonymous review", *out.ReviewedBy)
				}
			case out.ReviewedBy == nil || *out.ReviewedBy != tc.wantBy:
				t.Errorf("reviewed_by = %v, want %v", out.ReviewedBy, tc.wantBy)
			}
		})
	}
}

// An unknown payment id is a 404, not a silent success — a review that wrote
// nothing must not read as "verified".
func TestHandlerVerifyPayment_UnknownPaymentIs404(t *testing.T) {
	s := svc()
	st := s.store.(*fakeStore)
	st.payments[1] = &Payment{ID: 1, PaymentNumber: "PAY-1", PayableType: InvoiceModel, PayableID: 3}

	req := httptest.NewRequest(http.MethodPost, "/payments/999/verify", strings.NewReader(`{"approved":true}`))
	req.Header.Set("Content-Type", "application/json")
	req = req.WithContext(auth.ToContext(platformCtx(), &auth.User{ID: 3, Name: "Admin"}, 1))
	res := httptest.NewRecorder()
	verifyRouter(s).ServeHTTP(res, req)
	if res.Code != http.StatusNotFound {
		t.Fatalf("verify unknown payment = status %d body %s, want 404", res.Code, res.Body.String())
	}
	if len(st.paymentReviews) != 0 {
		t.Fatalf("unknown payment wrote a verdict: %v", st.paymentReviews)
	}
}
