package payments

import (
	"testing"

	"github.com/google/uuid"
)

// catalog fixtures: a chip-priced value (hints), a multi-method chip pack, a VK-only chip pack, a
// value with no CHIP price (misconfigured), and a pack whose only method is telegram.
func fixtureCatalog() []catalogEntry {
	value := catalogEntry{
		id:    uuid.MustParse("00000000-0000-0000-0000-000000000001"),
		title: "250 hints",
		atoms: []atomQty{{atomType: "hints", quantity: 250}},
		prices: []priceRow{
			{method: "", currency: CurrencyChip, amount: 500},
		},
	}
	pack := catalogEntry{
		id:    uuid.MustParse("00000000-0000-0000-0000-000000000002"),
		title: "100 chips",
		atoms: []atomQty{{atomType: "chips", quantity: 100}},
		prices: []priceRow{
			{method: "vk", currency: CurrencyVote, amount: 20},
			{method: "telegram", currency: CurrencyStar, amount: 25},
			{method: "direct", currency: CurrencyRUB, amount: 14900},
		},
	}
	vkOnlyPack := catalogEntry{
		id:    uuid.MustParse("00000000-0000-0000-0000-000000000003"),
		title: "VK-only chips",
		atoms: []atomQty{{atomType: "chips", quantity: 50}},
		prices: []priceRow{
			{method: "vk", currency: CurrencyVote, amount: 10},
		},
	}
	brokenValue := catalogEntry{
		id:    uuid.MustParse("00000000-0000-0000-0000-000000000004"),
		title: "no chip price",
		atoms: []atomQty{{atomType: "noads_days", quantity: 30}},
		// no CHIP price row — misconfigured, must be omitted
	}
	return []catalogEntry{value, pack, vkOnlyPack, brokenValue}
}

// byID indexes a projected storefront by product id for assertions.
func byID(v CatalogView) map[string]CatalogProduct {
	m := make(map[string]CatalogProduct, len(v.Products))
	for _, p := range v.Products {
		m[p.ProductID] = p
	}
	return m
}

func TestProjectCatalog_ContextMatrix(t *testing.T) {
	const (
		valueID  = "00000000-0000-0000-0000-000000000001"
		packID   = "00000000-0000-0000-0000-000000000002"
		vkPackID = "00000000-0000-0000-0000-000000000003"
		brokenID = "00000000-0000-0000-0000-000000000004"
	)
	entries := fixtureCatalog()

	tests := []struct {
		name        string
		cxt         Context
		wantPackAmt int64
		wantPackCur Currency
		wantVKPack  bool // the vk-only pack visible?
	}{
		{"vk", Context{Kind: SourceVK}, 20, CurrencyVote, true},
		{"vk-ios frozen still lists vk price", Context{Kind: SourceVK, Subtype: SubtypeIOS}, 20, CurrencyVote, true},
		{"telegram", Context{Kind: SourceTelegram}, 25, CurrencyStar, false},
		{"direct", Context{Kind: SourceDirect}, 14900, CurrencyRUB, false},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			got := byID(projectCatalog(entries, tc.cxt))

			// The value shows everywhere at its uniform chip price, never a money price.
			v, ok := got[valueID]
			if !ok {
				t.Fatalf("value missing from %s storefront", tc.name)
			}
			if v.Kind != "value" || v.Chips != 500 || v.MoneyCurrency != "" || v.MoneyAmount != 0 {
				t.Errorf("value = %+v, want kind=value chips=500 no money", v)
			}
			if len(v.Atoms) != 1 || v.Atoms[0].AtomType != "hints" || v.Atoms[0].Quantity != 250 {
				t.Errorf("value atoms = %+v, want [hints:250]", v.Atoms)
			}

			// The multi-method pack shows the context method's price, in that currency, no chips.
			p, ok := got[packID]
			if !ok {
				t.Fatalf("pack missing from %s storefront", tc.name)
			}
			if p.Kind != "pack" || p.Chips != 0 || p.MoneyAmount != tc.wantPackAmt || p.MoneyCurrency != string(tc.wantPackCur) {
				t.Errorf("pack = %+v, want kind=pack amount=%d currency=%s", p, tc.wantPackAmt, tc.wantPackCur)
			}

			// The vk-only pack shows only where a vk price exists.
			if _, ok := got[vkPackID]; ok != tc.wantVKPack {
				t.Errorf("vk-only pack present=%v, want %v (%s)", ok, tc.wantVKPack, tc.name)
			}

			// The misconfigured value (no CHIP price) is never shown.
			if _, ok := got[brokenID]; ok {
				t.Errorf("misconfigured value must be omitted (%s)", tc.name)
			}
		})
	}
}

// An untrusted context (empty Kind) has no payment method, so it shows values only — no packs.
func TestProjectCatalog_UntrustedShowsValuesOnly(t *testing.T) {
	got := byID(projectCatalog(fixtureCatalog(), Context{}))
	if _, ok := got["00000000-0000-0000-0000-000000000001"]; !ok {
		t.Error("untrusted context should still list the chip-priced value")
	}
	for _, id := range []string{
		"00000000-0000-0000-0000-000000000002",
		"00000000-0000-0000-0000-000000000003",
	} {
		if _, ok := got[id]; ok {
			t.Errorf("untrusted context must not list pack %s", id)
		}
	}
}

// An empty catalog projects to an empty storefront (no products seeded yet — the Release-1 state).
func TestProjectCatalog_Empty(t *testing.T) {
	if got := projectCatalog(nil, Context{Kind: SourceDirect}); len(got.Products) != 0 {
		t.Errorf("empty catalog projected %d products, want 0", len(got.Products))
	}
}

// TestProjectCatalog_CanonicalOrder checks the storefront lists products in the shared canonical order
// (compareCatalogRank): chip packs first ascending by rouble price, then chip-priced values grouped
// hints → no-ads → combo and ascending by chip price — the same order as the offer and the admin
// console, regardless of catalog creation order.
func TestProjectCatalog_CanonicalOrder(t *testing.T) {
	pack := func(title string, rub int64) catalogEntry {
		return catalogEntry{
			id:     uuid.New(),
			title:  title,
			atoms:  []atomQty{{atomType: atomChips, quantity: 1}},
			prices: []priceRow{{method: "direct", currency: CurrencyRUB, amount: rub}},
		}
	}
	value := func(title string, chips int64, atoms ...string) catalogEntry {
		e := catalogEntry{id: uuid.New(), title: title, prices: []priceRow{{method: "", currency: CurrencyChip, amount: chips}}}
		for _, a := range atoms {
			e.atoms = append(e.atoms, atomQty{atomType: a, quantity: 1})
		}
		return e
	}
	// Deliberately shuffled on input; the projection must impose the canonical order.
	entries := []catalogEntry{
		pack("packDear", 30000),
		value("bundle", 500, "hints", "noads_days"),
		pack("packCheap", 10000),
		value("adsOnly", 150, "noads_days"),
		value("hintsBig", 200, "hints"),
		value("hintsSmall", 50, "hints"),
	}
	got := projectCatalog(entries, Context{Kind: SourceDirect})
	want := []string{"packCheap", "packDear", "hintsSmall", "hintsBig", "adsOnly", "bundle"}
	if len(got.Products) != len(want) {
		t.Fatalf("projected %d products, want %d", len(got.Products), len(want))
	}
	for i, p := range got.Products {
		if p.Title != want[i] {
			t.Fatalf("order[%d] = %q, want %q (full: %v)", i, p.Title, want[i], productTitles(got.Products))
		}
	}
}

// productTitles extracts the projected product titles for a failure message.
func productTitles(products []CatalogProduct) []string {
	out := make([]string, len(products))
	for i, p := range products {
		out[i] = p.Title
	}
	return out
}

// TestSortAdminCatalog checks the admin list is ordered like the public offer: chip packs first,
// ascending by rouble price; then values, grouped (hints only -> no-ads only -> no-ads + hints) and
// ascending by chip price within a group. Stable and applied to active + archived alike.
func TestSortAdminCatalog(t *testing.T) {
	pack := func(title string, rub int64) AdminProduct {
		return AdminProduct{
			Title:  title,
			Atoms:  []AtomLine{{Atom: atomChips, Quantity: 1}},
			Prices: []PriceLine{{Method: string(SourceDirect), Currency: CurrencyRUB, Amount: rub}},
		}
	}
	value := func(title string, chips int64, atoms ...string) AdminProduct {
		p := AdminProduct{Title: title, Prices: []PriceLine{{Currency: CurrencyChip, Amount: chips}}}
		for _, a := range atoms {
			p.Atoms = append(p.Atoms, AtomLine{Atom: a, Quantity: 1})
		}
		return p
	}
	products := []AdminProduct{
		pack("packDear", 30000),
		value("bundle", 500, "hints", "noads_days"),
		pack("packCheap", 10000),
		value("adsOnly", 150, "noads_days"),
		value("hintsBig", 200, "hints"),
		value("hintsSmall", 50, "hints"),
	}
	SortAdminCatalog(products)
	want := []string{"packCheap", "packDear", "hintsSmall", "hintsBig", "adsOnly", "bundle"}
	for i, p := range products {
		if p.Title != want[i] {
			t.Fatalf("order[%d] = %q, want %q (full: %v)", i, p.Title, want[i], titles(products))
		}
	}
}

// titles extracts the product titles for a failure message.
func titles(products []AdminProduct) []string {
	out := make([]string, len(products))
	for i, p := range products {
		out[i] = p.Title
	}
	return out
}
