package reports

// Dashboard summary ↔ drilldown parity (AGENTS.md §8).
//
// The 1 Nov report was "the card shows a number, the detail is all zero": the
// cards were computed from proxies (month/30, len(activity), a hardcoded 2)
// while the modals read arrays the handler left empty. This test pins the
// contract at the layer that broke — the payload handed to the dashboard —
// by serving the same store through both endpoints and comparing the card
// value with the rows behind it.

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

	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/shopspring/decimal"
)

type parityStore struct {
	*fakeStore
	todaySum    decimal.Decimal
	active      []RecentOrder
	today       []RecentOrder
	pos         []ActivePO
	invoices    []PendingInvoice
	cashIncome  []float64
	cashExpense []float64
}

func (p *parityStore) TodaySales(context.Context, time.Time) (decimal.Decimal, error) {
	return p.todaySum, nil
}
func (p *parityStore) PendingOrdersCount(context.Context) (int, error) { return len(p.active), nil }
func (p *parityStore) ActivePOsCount(context.Context) (int, error)     { return len(p.pos), nil }

func (p *parityStore) Pool() *pgxpool.Pool { return nil }

func (p *parityStore) OverdueInvoicesCount(context.Context, time.Time) (int, error) {
	n := 0
	for _, i := range p.invoices {
		if i.IsOverdue {
			n++
		}
	}
	return n, nil
}
func (p *parityStore) MonthNetCashflow(context.Context, time.Time, time.Time) (decimal.Decimal, error) {
	// The same arithmetic the cashflow series carries: last slot net.
	income, expense := p.lastMonth()
	return income.Sub(expense), nil
}
func (p *parityStore) ChartsExtras(context.Context, time.Time, time.Time, time.Time, time.Time) (*ChartsExtras, error) {
	return &ChartsExtras{
		CashflowIncome:  p.cashIncome,
		CashflowExpense: p.cashExpense,
		ActiveOrders:    p.active,
		TodaySales:      p.today,
		PendingInvoices: p.invoices,
		ActivePOs:       p.pos,
	}, nil
}
func (p *parityStore) lastMonth() (decimal.Decimal, decimal.Decimal) {
	if len(p.cashIncome) == 0 || len(p.cashExpense) == 0 {
		return decimal.Zero, decimal.Zero
	}
	return decimal.NewFromFloat(p.cashIncome[len(p.cashIncome)-1]),
		decimal.NewFromFloat(p.cashExpense[len(p.cashExpense)-1])
}

func dec(n int64) decimal.Decimal { return decimal.NewFromInt(n) }

type dashboardPayload struct {
	Sales struct {
		TodayRevenue    decimal.Decimal `json:"todayRevenue"`
		PendingOrders   int             `json:"pendingOrders"`
		TotalReceivable decimal.Decimal `json:"totalReceivable"`
		OverdueCount    int             `json:"overdueCount"`
	} `json:"sales"`
	Purchasing struct {
		ActivePOs int `json:"activePOs"`
	} `json:"purchasing"`
	Finance struct {
		NetCashflow decimal.Decimal `json:"netCashflow"`
	} `json:"finance"`
}

type chartsPayload struct {
	ActiveOrders []struct {
		ID int64 `json:"id"`
	} `json:"activeOrders"`
	TodaySales []struct {
		TotalAmount decimal.Decimal `json:"total_amount"`
	} `json:"todaySales"`
	PendingInvoices []struct {
		RemainingAmount decimal.Decimal `json:"remaining_amount"`
		IsOverdue       bool            `json:"is_overdue"`
	} `json:"pendingInvoices"`
	ActivePOs []struct {
		ID int64 `json:"id"`
	} `json:"activePOs"`
	Cashflow struct {
		Income  []float64 `json:"income"`
		Expense []float64 `json:"expense"`
	} `json:"cashflow"`
}

func serveDashboard(t *testing.T, h http.HandlerFunc, path string) json.RawMessage {
	t.Helper()
	rec := httptest.NewRecorder()
	h(rec, httptest.NewRequest(http.MethodGet, path, nil))
	if rec.Code != http.StatusOK {
		t.Fatalf("%s = %d, want 200: %s", path, rec.Code, rec.Body.String())
	}
	var env struct {
		Data json.RawMessage `json:"data"`
	}
	if err := json.Unmarshal(rec.Body.Bytes(), &env); err != nil {
		t.Fatalf("%s: %v", path, err)
	}
	return env.Data
}

func TestDashboard_SummaryMatchesDrilldown(t *testing.T) {
	st := &parityStore{
		fakeStore: &fakeStore{
			monthSales:    dec(300000),
			monthPurchase: dec(100000),
			receiv:        dec(250000),
			payables:      dec(90000),
		},
		todaySum: dec(10000),
		active: []RecentOrder{
			{ID: 1, SoNumber: "SO-1", Status: "pending_approval"},
			{ID: 2, SoNumber: "SO-2", Status: "picking"},
			{ID: 3, SoNumber: "SO-3", Status: "shipped"},
		},
		today: []RecentOrder{
			{ID: 4, SoNumber: "SO-4", TotalAmount: dec(4000)},
			{ID: 5, SoNumber: "SO-5", TotalAmount: dec(6000)},
		},
		pos: []ActivePO{
			{ID: 10, PONumber: "PO-1", RemainingAmount: dec(50000)},
			{ID: 11, PONumber: "PO-2", RemainingAmount: dec(70000)},
		},
		invoices: []PendingInvoice{
			{ID: 20, InvoiceNumber: "INV-1", RemainingAmount: dec(100000), IsOverdue: true},
			{ID: 21, InvoiceNumber: "INV-2", RemainingAmount: dec(150000)},
		},
		cashIncome:  []float64{0, 0, 200000},
		cashExpense: []float64{0, 0, 50000},
	}
	h := NewHandler(NewService(st, loc()))

	var k dashboardPayload
	if err := json.Unmarshal(serveDashboard(t, h.KPIs, "/dashboard/kpis"), &k); err != nil {
		t.Fatal(err)
	}
	var c chartsPayload
	if err := json.Unmarshal(serveDashboard(t, h.Charts, "/dashboard/charts"), &c); err != nil {
		t.Fatal(err)
	}

	// Order Aktif: the count and the list are the same set.
	if k.Sales.PendingOrders != len(c.ActiveOrders) {
		t.Errorf("PendingOrders = %d, activeOrders rows = %d", k.Sales.PendingOrders, len(c.ActiveOrders))
	}
	if len(c.ActiveOrders) == 0 {
		t.Error("activeOrders is empty — the Order Aktif modal would render 0")
	}

	// Penjualan Hari Ini: the rows sum to the card.
	var todaySum decimal.Decimal
	for _, o := range c.TodaySales {
		todaySum = todaySum.Add(o.TotalAmount)
	}
	if !todaySum.Equal(k.Sales.TodayRevenue) {
		t.Errorf("todayRevenue = %s, todaySales rows sum = %s", k.Sales.TodayRevenue, todaySum)
	}

	// Piutang: the rows sum to the card, and overdue matches the hint.
	var receivableSum decimal.Decimal
	overdue := 0
	for _, i := range c.PendingInvoices {
		receivableSum = receivableSum.Add(i.RemainingAmount)
		if i.IsOverdue {
			overdue++
		}
	}
	if !receivableSum.Equal(k.Sales.TotalReceivable) {
		t.Errorf("totalReceivable = %s, pendingInvoices sum = %s", k.Sales.TotalReceivable, receivableSum)
	}
	if overdue != k.Sales.OverdueCount {
		t.Errorf("overdueCount = %d, overdue rows = %d", k.Sales.OverdueCount, overdue)
	}

	// PO Aktif: the list is the counted set.
	if k.Purchasing.ActivePOs != len(c.ActivePOs) {
		t.Errorf("activePOs card = %d, rows = %d", k.Purchasing.ActivePOs, len(c.ActivePOs))
	}

	// Net cashflow: the card equals the current month of the series the
	// CashflowModal renders (its last slot).
	last := len(c.Cashflow.Income) - 1
	net := decimal.NewFromFloat(c.Cashflow.Income[last]).Sub(decimal.NewFromFloat(c.Cashflow.Expense[last]))
	if !net.Equal(k.Finance.NetCashflow) {
		t.Errorf("netCashflow = %s, cashflow series current month = %s", k.Finance.NetCashflow, net)
	}
}

