package repositories

import (
	"testing"
	"time"
)

// The terminal holds a unique index on barcode, so this rule decides whether a
// catalogue import succeeds at all. Measured against live data when it was
// written: 6,245 products, 93 distinct SKUs, and "1" used by 5,794 of them.
func TestPosBarcodeFallsBackToProductIdWhenTheSkuIsNotScannable(t *testing.T) {
	cases := []struct {
		name      string
		productID int
		sku       string
		want      string
	}{
		{"the SKU almost every product shares", 844, "1", "844"},
		{"blank SKU", 845, "", "845"},
		{"whitespace only", 846, "   ", "846"},
		{"too short to be a barcode", 847, "1234567", "847"},
		{"too long to be a barcode", 848, "123456789012345", "848"},
		{"not digits", 849, "SKU-ABC-123", "849"},
		{"digits with a space", 850, "1234 5678", "850"},

		// Real scannable codes are used as-is, so the day the catalogue carries
		// them scanning starts working with no code change.
		{"EAN-8", 851, "12345678", "12345678"},
		{"UPC-A", 852, "012345678905", "012345678905"},
		{"EAN-13", 853, "8901030865278", "8901030865278"},
		{"padded EAN-13", 854, "  8901030865278  ", "8901030865278"},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			if got := posBarcode(c.productID, c.sku); got != c.want {
				t.Errorf("posBarcode(%d, %q) = %q, want %q", c.productID, c.sku, got, c.want)
			}
		})
	}
}

func TestPosBarcodesAreUniqueAcrossACatalogueOfSharedSkus(t *testing.T) {
	// The failure this exists to prevent: a whole catalogue collapsing onto one
	// barcode and the import being rejected by the terminal's unique index.
	seen := make(map[string]int)
	for id := 844; id < 844+500; id++ {
		barcode := posBarcode(id, "1")
		if first, clash := seen[barcode]; clash {
			t.Fatalf("products %d and %d both produced barcode %q", first, id, barcode)
		}
		seen[barcode] = id
	}
}

func TestRoundStockQtyNeverUnderDeducts(t *testing.T) {
	// productstocks.quantity is an integer column and a counter sells 1.5 kg of
	// onions. Rounding up keeps recorded stock at or below what is on the shelf;
	// truncating would let the shop oversell a little more with every sale.
	cases := []struct {
		quantity float64
		want     int
	}{
		{1, 1},
		{1.5, 2},
		{0.25, 1},
		{2.0, 2},
		{2.01, 3},
		{0, 1},
		{-1, 1},
	}

	for _, c := range cases {
		if got := roundStockQty(c.quantity); got != c.want {
			t.Errorf("roundStockQty(%g) = %d, want %d", c.quantity, got, c.want)
		}
	}
}

func TestLegacyOrderQtyIsUnchanged(t *testing.T) {
	// App orders have always truncated, and that behaviour is deliberately
	// preserved rather than corrected — changing it would silently alter stock
	// deduction for every order already flowing through createOrderTx.
	cases := []struct {
		quantity float64
		want     int
	}{
		{1, 1},
		{1.5, 1},
		{0.5, 1},
		{3.9, 3},
		{0, 1},
	}

	for _, c := range cases {
		if got := legacyOrderQty(c.quantity); got != c.want {
			t.Errorf("legacyOrderQty(%g) = %d, want %d", c.quantity, got, c.want)
		}
	}
}

// A catalogue revision is the terminal's memory of when it last pulled. If it
// does not survive a round trip, every pull silently becomes a full snapshot —
// or worse, a filtered result gets labelled as one and the shop's shelf empties.
func TestPosRevisionRoundTrips(t *testing.T) {
	at := time.Date(2026, 8, 3, 12, 30, 45, 0, time.UTC)

	revision := posRevisionFor(1135, at)
	if revision != "loc1135-20260803T123045Z" {
		t.Fatalf("revision = %q, want loc1135-20260803T123045Z", revision)
	}

	got := posRevisionCutoff(1135, revision)
	if !got.Equal(at) {
		t.Errorf("cutoff = %v, want %v", got, at)
	}
}

func TestAnUnusableRevisionFallsBackToAFullSnapshot(t *testing.T) {
	// A zero cutoff means "send everything", and the caller turns that into
	// is_delta:false. Falling back the other way — answering an unreadable
	// revision with a change set — would leave a terminal permanently missing
	// every change it had not already seen, with nothing to show for it.
	cases := []struct {
		name     string
		location int
		revision string
	}{
		{"empty", 1135, ""},
		{"whitespace", 1135, "   "},
		{"no prefix", 1135, "20260803T123045Z"},
		{"malformed timestamp", 1135, "loc1135-not-a-time"},
		{"truncated timestamp", 1135, "loc1135-20260803"},
		{"another outlet's revision", 1135, "loc1097-20260803T123045Z"},
		{"prefix collision", 113, "loc1135-20260803T123045Z"},
		{"garbage", 1135, "../../etc/passwd"},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			if got := posRevisionCutoff(c.location, c.revision); !got.IsZero() {
				t.Errorf("cutoff = %v, want zero (full snapshot) for %q", got, c.revision)
			}
		})
	}
}

func TestAnOutletCannotReplayAnotherOutletsRevision(t *testing.T) {
	// loc1135 and loc113 share a textual prefix. Matching loosely would let one
	// shop's cutoff silently scope another shop's delta.
	at := time.Date(2026, 8, 3, 12, 30, 45, 0, time.UTC)
	revision := posRevisionFor(1135, at)

	if got := posRevisionCutoff(1135, revision); got.IsZero() {
		t.Error("the issuing outlet could not read back its own revision")
	}
	for _, other := range []int{113, 11350, 1097, 1} {
		if got := posRevisionCutoff(other, revision); !got.IsZero() {
			t.Errorf("outlet %d accepted outlet 1135's revision (cutoff %v)", other, got)
		}
	}
}
