package reports

import (
	"testing"

	"github.com/shopspring/decimal"
)

func TestClassifyCounterpart(t *testing.T) {
	cases := []struct {
		code, want, why string
	}{
		{"4000.01", CashflowSectionOperating, "revenue is day-to-day trading"},
		{"5000.01", CashflowSectionOperating, "expense is day-to-day trading"},
		{"110302", CashflowSectionOperating, "receivable settles into operating"},
		{"", CashflowSectionOperating, "unknown still moved cash, so it is listed not hidden"},
		{"120101", CashflowSectionInvesting, "fixed assets are investing"},
		{"150101", CashflowSectionInvesting, "long-term investment is investing"},
		{"210102", CashflowSectionOperating, "trade supplier debt is operating outflow"},
		{"220101", CashflowSectionFinancing, "long-term bank debt is financing"},
		{"3000.01", CashflowSectionFinancing, "equity is financing"},
	}
	for _, tc := range cases {
		if got := classifyCounterpart(tc.code); got != tc.want {
			t.Errorf("classifyCounterpart(%q) = %q, want %q (%s)", tc.code, got, tc.want, tc.why)
		}
	}
}

func TestIsCashCodeExcludesReceivablesAndAssets(t *testing.T) {
	// This is the whole reason cash codes are an explicit range and not
	// LIKE '1%': in this chart 110302 and 120101 begin with 1 and are not cash.
	for _, code := range []string{"1000.01.01", "1000.03.01", "1000.01.02", "1000.03.02"} {
		if !isCashCode(code) {
			t.Errorf("isCashCode(%q) = false, want true", code)
		}
	}
	for _, code := range []string{"110302", "110401", "120101", "120102", "210102", "4000.01"} {
		if isCashCode(code) {
			t.Errorf("isCashCode(%q) = true, want false — this is not cash", code)
		}
	}
}

func TestSectionAccumulatesSignedAmounts(t *testing.T) {
	s := newSection(CashflowSectionOperating)
	s.add(CashflowStatementItem{Amount: decimal.NewFromInt(1000)})
	s.add(CashflowStatementItem{Amount: decimal.NewFromInt(-400)})
	s.add(CashflowStatementItem{Amount: decimal.NewFromInt(250)})
	s.finish()

	if !s.Inflow.Equal(decimal.NewFromInt(1250)) {
		t.Errorf("Inflow = %s, want 1250", s.Inflow)
	}
	if !s.Outflow.Equal(decimal.NewFromInt(400)) {
		t.Errorf("Outflow = %s, want 400", s.Outflow)
	}
	if !s.Net.Equal(decimal.NewFromInt(850)) {
		t.Errorf("Net = %s, want 850 — net must equal inflow minus outflow", s.Net)
	}
	// The defect this endpoint exists to prevent: a total that disagrees with
	// its own parts.
	if !s.Net.Equal(s.Inflow.Sub(s.Outflow)) {
		t.Errorf("Net %s != Inflow %s - Outflow %s", s.Net, s.Inflow, s.Outflow)
	}
}

func TestNewSectionHasNonNilItems(t *testing.T) {
	// The ERP iterates section.items directly; a nil slice renders as null and
	// blows up the v-for on an empty section.
	for _, s := range []CashflowStatementSection{newSection(CashflowSectionOperating), newSection(CashflowSectionInvesting), newSection(CashflowSectionFinancing)} {
		if s.Items == nil {
			t.Errorf("section %s has nil Items, want empty slice", s.Section)
		}
	}
}

// The defect this endpoint exists to remove was a statement whose total did not
// equal its own parts. These lock the invariant at the level where it can be
// checked without a database.
func TestStatementNetAlwaysEqualsItsSections(t *testing.T) {
	stmt := &CashflowStatement{
		Operating: newSection(CashflowSectionOperating),
		Investing: newSection(CashflowSectionInvesting),
		Financing: newSection(CashflowSectionFinancing),
	}
	// Deliberately mixed, including an outflow, because the original bug was
	// that an outflow contributed nothing (debit is 0 on a credit line).
	stmt.Operating.add(CashflowStatementItem{Amount: decimal.NewFromInt(2_850_000)})
	stmt.Operating.add(CashflowStatementItem{Amount: decimal.NewFromInt(-450_000)})
	stmt.Operating.add(CashflowStatementItem{Amount: decimal.NewFromInt(-350_000)})
	stmt.Financing.add(CashflowStatementItem{Amount: decimal.NewFromInt(-7_500_000)})
	stmt.Financing.add(CashflowStatementItem{Amount: decimal.NewFromInt(500_000)})
	stmt.Operating.finish()
	stmt.Investing.finish()
	stmt.Financing.finish()

	stmt.NetCashFlow = stmt.Operating.Net.Add(stmt.Investing.Net).Add(stmt.Financing.Net)
	if !stmt.NetCashFlow.Equal(decimal.NewFromInt(-4_950_000)) {
		t.Fatalf("net = %s, want -4950000", stmt.NetCashFlow)
	}

	sum := stmt.Operating.Net.Add(stmt.Investing.Net).Add(stmt.Financing.Net)
	if !stmt.NetCashFlow.Equal(sum) {
		t.Errorf("net %s != A+B+C %s", stmt.NetCashFlow, sum)
	}

	// Outflow must be non-zero: this is the assertion that catches a credit line
	// being read as its (zero) debit side.
	if stmt.Operating.Outflow.Sign() == 0 {
		t.Error("operating outflow is 0, want the two expense payments counted")
	}
	if !stmt.Operating.Outflow.Equal(decimal.NewFromInt(800_000)) {
		t.Errorf("operating outflow = %s, want 800000", stmt.Operating.Outflow)
	}
}

// Beginning + movement must equal ending, by construction rather than luck.
func TestEndingCashIsBeginningPlusNet(t *testing.T) {
	beginning := decimal.NewFromInt(1_250_000)
	net := decimal.NewFromInt(750_000)
	ending := beginning.Add(net)
	if !ending.Equal(decimal.NewFromInt(2_000_000)) {
		t.Errorf("ending = %s, want 2000000", ending)
	}
}

func TestCounterpartOfNonCashLineStillClassified(t *testing.T) {
	// A cash-side line of a transfer has no real counterpart and must not be
	// counted as a movement; isCashCode is what the service checks on.
	if !isCashCode("1000.01.01") {
		t.Fatal("expected a cash code")
	}
	// Every non-1000 code routes somewhere rather than vanishing.
	for _, code := range []string{"", "4000.01", "110302", "120101", "210102"} {
		if classifyCounterpart(code) == "" {
			t.Errorf("classifyCounterpart(%q) returned empty, want a section", code)
		}
	}
}
