//go:build integration

package inttest

import (
	"context"
	"fmt"
	"strings"
	"testing"
	"time"

	"github.com/google/uuid"

	"scrabble/backend/internal/payments"
)

// seedLedgerRow writes one ledger row directly, at a chosen age, so a test can stand up a history
// that spans a date range without waiting for one.
func seedLedgerRow(t *testing.T, acc uuid.UUID, kind, source, provider string, chips int, snapshot string, age time.Duration) {
	t.Helper()
	var providerPaymentID any
	if provider != "" {
		providerPaymentID = "pp-" + uuid.NewString()
	}
	if _, err := testDB.ExecContext(context.Background(), `
		INSERT INTO payments.ledger (ledger_id, account_id, kind, source, origin, chips_delta,
		                             provider, provider_payment_id, snapshot, created_at)
		VALUES ($1,$2,$3,$4,$4,$5,NULLIF($6,''),$7,$8::jsonb, now() - $9::interval)`,
		uuid.New(), acc, kind, source, chips, provider, providerPaymentID, snapshot,
		fmt.Sprintf("%d seconds", int(age.Seconds()))); err != nil {
		t.Fatalf("seed ledger row: %v", err)
	}
}

// ledgerReport reads a filtered page through the service, the way the console does.
func ledgerReport(t *testing.T, pay *payments.Service, f payments.LedgerFilter, limit, offset int) payments.LedgerReport {
	t.Helper()
	rep, err := pay.LedgerReportPage(context.Background(), f, limit, offset)
	if err != nil {
		t.Fatalf("ledger report: %v", err)
	}
	return rep
}

// TestLedgerReportFilters covers each filter axis over one seeded history, including the property
// that matters most for an operator: the totals describe the whole filtered range, not the page.
func TestLedgerReportFilters(t *testing.T) {
	acc, other := provisionAccount(t), provisionAccount(t)
	// Two recent purchases on different rails, one older purchase outside a narrow range, a spend
	// (no rail at all) and a refund.
	seedLedgerRow(t, acc, "fund", "direct", "yookassa", 100, `{"amount_minor":14900,"currency":"RUB","title":"100 chips"}`, time.Hour)
	seedLedgerRow(t, acc, "fund", "vk", "vk", 50, `{"amount_minor":50,"currency":"VOTE","title":"50 chips"}`, 2*time.Hour)
	seedLedgerRow(t, other, "fund", "direct", "yookassa", 30, `{"amount_minor":4900,"currency":"RUB","title":"30 chips"}`, 40*24*time.Hour)
	seedLedgerRow(t, acc, "spend", "direct", "", -20, `{"title":"hints"}`, 30*time.Minute)
	seedLedgerRow(t, acc, "refund", "direct", "yookassa", -100, `{"amount_minor":14900,"currency":"RUB"}`, 10*time.Minute)

	pay := newPaymentsService()
	// Scoped to this test's own account: the suite shares one database and other tests seed recent
	// ledger rows, so an unscoped count would be a coin toss.
	recent := payments.LedgerFilter{From: time.Now().Add(-24 * time.Hour), AccountID: acc}

	t.Run("date range excludes older rows", func(t *testing.T) {
		rep := ledgerReport(t, pay, recent, 100, 0)
		for _, r := range rep.Rows {
			if r.AccountID == other {
				t.Error("a row from outside the range was returned")
			}
		}
	})

	t.Run("account", func(t *testing.T) {
		rep := ledgerReport(t, pay, payments.LedgerFilter{AccountID: other}, 100, 0)
		if rep.Total != 1 {
			t.Fatalf("rows for the other account = %d, want 1", rep.Total)
		}
		rep = ledgerReport(t, pay, payments.LedgerFilter{AccountID: acc}, 100, 0)
		if rep.Total != 4 {
			t.Fatalf("rows for the account = %d, want 4", rep.Total)
		}
	})

	t.Run("kind", func(t *testing.T) {
		f := recent
		f.Kind = "spend"
		rep := ledgerReport(t, pay, f, 100, 0)
		for _, r := range rep.Rows {
			if r.Kind != "spend" {
				t.Errorf("kind filter returned a %q row", r.Kind)
			}
		}
		if rep.Total == 0 {
			t.Error("kind filter matched nothing")
		}
	})

	t.Run("wallet", func(t *testing.T) {
		f := recent
		f.Wallet = "vk"
		rep := ledgerReport(t, pay, f, 100, 0)
		if rep.Total != 1 {
			t.Fatalf("vk wallet rows = %d, want 1", rep.Total)
		}
	})

	t.Run("rail excludes spends, which have none", func(t *testing.T) {
		f := recent
		f.Provider = "yookassa"
		rep := ledgerReport(t, pay, f, 100, 0)
		for _, r := range rep.Rows {
			if r.Kind == "spend" {
				t.Error("a chip spend matched a rail filter, but a spend has no rail")
			}
		}
	})

	t.Run("totals cover the range, not the page", func(t *testing.T) {
		f := recent
		full := ledgerReport(t, pay, f, 100, 0)
		firstPage := ledgerReport(t, pay, f, 1, 0)
		if len(firstPage.Rows) != 1 {
			t.Fatalf("page rows = %d, want 1", len(firstPage.Rows))
		}
		if firstPage.Total != full.Total {
			t.Errorf("page total = %d, want the full match count %d", firstPage.Total, full.Total)
		}
		if firstPage.Totals.ChipsIn != full.Totals.ChipsIn || firstPage.Totals.ChipsOut != full.Totals.ChipsOut {
			t.Errorf("page totals = %+v, want the whole range's %+v", firstPage.Totals, full.Totals)
		}
		// 100 + 50 credited, 20 spent + 100 refunded revoked.
		if full.Totals.ChipsIn != 150 || full.Totals.ChipsOut != 120 {
			t.Errorf("chip totals = in %d / out %d, want 150 / 120", full.Totals.ChipsIn, full.Totals.ChipsOut)
		}
		// Money is kept per currency; summing roubles and Votes together would be meaningless.
		var rub, vote bool
		for _, m := range full.Totals.MoneyIn {
			switch m.Currency() {
			case payments.CurrencyRUB:
				rub = true
				if m.Minor() != 14900 {
					t.Errorf("RUB in = %d, want 14900", m.Minor())
				}
			case payments.CurrencyVote:
				vote = true
			}
		}
		if !rub || !vote {
			t.Errorf("money in = %v, want both a RUB and a VOTE total", full.Totals.MoneyIn)
		}
		if len(full.Totals.MoneyRefunded) != 1 || full.Totals.MoneyRefunded[0].Minor() != 14900 {
			t.Errorf("money refunded = %v, want one 149.00 RUB total", full.Totals.MoneyRefunded)
		}
	})

	t.Run("money is recovered from the snapshot", func(t *testing.T) {
		f := recent
		f.Kind = "fund"
		f.Wallet = "direct"
		rep := ledgerReport(t, pay, f, 100, 0)
		if len(rep.Rows) != 1 {
			t.Fatalf("rows = %d, want 1", len(rep.Rows))
		}
		r := rep.Rows[0]
		if !r.HasMoney() || r.Money.Minor() != 14900 || r.Money.Currency() != payments.CurrencyRUB {
			t.Errorf("row money = %v, want 149.00 RUB", r.Money)
		}
	})

	t.Run("a spend carries no money", func(t *testing.T) {
		f := recent
		f.Kind = "spend"
		rep := ledgerReport(t, pay, f, 100, 0)
		if len(rep.Rows) != 1 {
			t.Fatalf("rows = %d, want 1", len(rep.Rows))
		}
		if rep.Rows[0].HasMoney() {
			t.Errorf("a chip spend reported money %v", rep.Rows[0].Money)
		}
	})
}

// TestLedgerReportPaging checks paging walks the range without repeating or dropping a row.
func TestLedgerReportPaging(t *testing.T) {
	acc := provisionAccount(t)
	for i := range 5 {
		seedLedgerRow(t, acc, "fund", "direct", "yookassa", 10,
			`{"amount_minor":1000,"currency":"RUB"}`, time.Duration(i+1)*time.Minute)
	}
	pay := newPaymentsService()
	f := payments.LedgerFilter{AccountID: acc}

	seen := map[string]bool{}
	for offset := 0; offset < 5; offset += 2 {
		rep := ledgerReport(t, pay, f, 2, offset)
		if rep.Total != 5 {
			t.Fatalf("total = %d, want 5 at every offset", rep.Total)
		}
		for _, r := range rep.Rows {
			key := r.ProviderPaymentID
			if seen[key] {
				t.Errorf("row %s appeared on two pages", key)
			}
			seen[key] = true
		}
	}
	if len(seen) != 5 {
		t.Errorf("walked %d rows across the pages, want 5", len(seen))
	}
}

// TestLedgerConsolePageAndExport drives the console: the page renders under a filter, the CSV export
// honours that same filter, and the user card links to the ledger scoped to the account instead of
// listing the rows itself.
func TestLedgerConsolePageAndExport(t *testing.T) {
	srv, _, _ := bannerServer(t)
	acc, other := provisionAccount(t), provisionAccount(t)
	seedLedgerRow(t, acc, "fund", "direct", "yookassa", 100, `{"amount_minor":14900,"currency":"RUB","title":"Pack A"}`, time.Hour)
	seedLedgerRow(t, other, "fund", "vk", "vk", 50, `{"amount_minor":50,"currency":"VOTE","title":"Pack B"}`, time.Hour)

	rec := consoleGet(t, srv, "/_gm/ledger")
	if rec.Code != 200 {
		t.Fatalf("ledger page = %d, want 200", rec.Code)
	}
	body := rec.Body.String()
	for _, want := range []string{"Pack A", "Pack B", "149.00 RUB", "Totals for the filtered range"} {
		if !strings.Contains(body, want) {
			t.Errorf("ledger page does not show %q", want)
		}
	}

	// Filtering to one account must drop the other's operation from the page.
	rec = consoleGet(t, srv, "/_gm/ledger?user="+acc.String())
	if body := rec.Body.String(); !strings.Contains(body, "Pack A") || strings.Contains(body, "Pack B") {
		t.Error("the user filter did not scope the ledger page to one account")
	}

	// The pager links must carry the filters, or paging would silently widen the view.
	if body := rec.Body.String(); strings.Contains(body, "page=") && !strings.Contains(body, "user="+acc.String()+"&amp;page=") {
		t.Error("a pager link dropped the active filters")
	}

	// The export honours the same filter as the screen it is linked from.
	rec = consoleGet(t, srv, "/_gm/ledger.csv?user="+acc.String())
	if rec.Code != 200 {
		t.Fatalf("ledger export = %d, want 200", rec.Code)
	}
	csv := rec.Body.String()
	if !strings.Contains(csv, "Pack A") || strings.Contains(csv, "Pack B") {
		t.Error("the CSV export ignored the filter")
	}
	if !strings.Contains(csv, "amount_minor,currency") {
		t.Error("the CSV export has no money columns")
	}

	// The user card is a summary now: totals and a link into the ledger, not the rows themselves.
	rec = consoleGet(t, srv, "/_gm/users/"+acc.String())
	card := rec.Body.String()
	if !strings.Contains(card, "/_gm/ledger?user="+acc.String()) {
		t.Error("the user card does not link to its ledger slice")
	}
	if !strings.Contains(card, "Chips credited") {
		t.Error("the user card shows no totals summary")
	}
}
