package storage

import (
	"errors"
	"testing"
	"time"
)

func TestMischiefOrderLifecycle(t *testing.T) {
	setupTestDB(t)

	o, err := InsertMischiefOrder("sub-1", "reala", "tok-josie", "Josie", "elite", true)
	if err != nil {
		t.Fatal(err)
	}
	if o.Status != MischiefPending {
		t.Fatalf("fresh order status = %q, want pending", o.Status)
	}
	if !o.Signed {
		t.Error("signed flag lost through insert")
	}

	pending, err := PendingMischiefOrders(10)
	if err != nil {
		t.Fatal(err)
	}
	if len(pending) != 1 || pending[0].GUID != o.GUID {
		t.Fatalf("pending = %+v, want the one order we just placed", pending)
	}

	// gogobee places the contract.
	got, err := ResolveMischiefOrder(o.GUID, MischiefPlaced, "the word is out")
	if err != nil {
		t.Fatal(err)
	}
	if got.Status != MischiefPlaced || got.Detail != "the word is out" {
		t.Fatalf("resolved order = %+v, want placed with detail", got)
	}

	// It must leave the pending set.
	if pending, _ := PendingMischiefOrders(10); len(pending) != 0 {
		t.Fatalf("placed order still pending: %+v", pending)
	}
}

// TestMischiefResolveIsIdempotent is the whole reason the guid is an end-to-end
// key: gogobee's poll loop retries, so a verdict can arrive twice, and the second
// arrival must not overwrite the first or error.
func TestMischiefResolveIsIdempotent(t *testing.T) {
	setupTestDB(t)

	o, err := InsertMischiefOrder("sub-1", "reala", "tok", "Josie", "grunt", false)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := ResolveMischiefOrder(o.GUID, MischiefPlaced, "first"); err != nil {
		t.Fatal(err)
	}
	// A second, *different* verdict arrives. First one wins.
	got, err := ResolveMischiefOrder(o.GUID, MischiefBouncedFunds, "second")
	if err != nil {
		t.Fatalf("re-resolve errored: %v", err)
	}
	if got.Status != MischiefPlaced || got.Detail != "first" {
		t.Fatalf("idempotency broken: order became %+v", got)
	}
}

func TestMischiefResolveUnknownAndBadVerdict(t *testing.T) {
	setupTestDB(t)

	if _, err := ResolveMischiefOrder("nope", MischiefPlaced, ""); !errors.Is(err, ErrNoSuchOrder) {
		t.Fatalf("unknown guid err = %v, want ErrNoSuchOrder", err)
	}

	o, _ := InsertMischiefOrder("sub-1", "reala", "tok", "Josie", "grunt", false)
	if _, err := ResolveMischiefOrder(o.GUID, "exploded", ""); err == nil {
		t.Error("a bogus verdict status was accepted")
	}
	// The order must survive a rejected verdict as still-pending.
	if got, _ := MischiefOrderByGUID(o.GUID); got.Status != MischiefPending {
		t.Fatalf("order moved off pending on a bad verdict: %q", got.Status)
	}
}

func TestMischiefOrdersByBuyerAndCount(t *testing.T) {
	setupTestDB(t)

	for i := 0; i < 3; i++ {
		if _, err := InsertMischiefOrder("sub-A", "alice", "tok", "Josie", "grunt", false); err != nil {
			t.Fatal(err)
		}
	}
	if _, err := InsertMischiefOrder("sub-B", "bob", "tok", "Josie", "grunt", false); err != nil {
		t.Fatal(err)
	}

	mine, err := MischiefOrdersByBuyer("sub-A", 20)
	if err != nil {
		t.Fatal(err)
	}
	if len(mine) != 3 {
		t.Fatalf("alice sees %d orders, want 3 (and none of bob's)", len(mine))
	}

	n, err := CountMischiefOrdersSince("sub-A", time.Now().Add(-time.Hour).Unix())
	if err != nil {
		t.Fatal(err)
	}
	if n != 3 {
		t.Fatalf("count since an hour ago = %d, want 3", n)
	}
	if n, _ := CountMischiefOrdersSince("sub-A", time.Now().Add(time.Hour).Unix()); n != 0 {
		t.Fatalf("count since the future = %d, want 0", n)
	}
}

func TestMischiefTiersReplaceAndLookup(t *testing.T) {
	setupTestDB(t)

	tiers := []MischiefTier{
		{Key: "grunt", Display: "Grunt", Fee: 40, SignedFee: 50, Blurb: "theatre"},
		{Key: "boss", Display: "Boss", Fee: 1200, SignedFee: 1500},
	}
	if err := ReplaceMischiefTiers(tiers); err != nil {
		t.Fatal(err)
	}

	got, err := MischiefTiers()
	if err != nil {
		t.Fatal(err)
	}
	if len(got) != 2 || got[0].Key != "grunt" || got[1].Key != "boss" {
		t.Fatalf("catalog order not preserved: %+v", got)
	}

	tier, ok, err := MischiefTierByKey("boss")
	if err != nil || !ok || tier.SignedFee != 1500 {
		t.Fatalf("lookup boss = %+v ok=%v err=%v", tier, ok, err)
	}
	if _, ok, _ := MischiefTierByKey("dragon"); ok {
		t.Error("lookup invented a tier that was never pushed")
	}

	// Replace, never merge: a dropped tier vanishes.
	if err := ReplaceMischiefTiers([]MischiefTier{{Key: "grunt", Display: "Grunt", Fee: 40, SignedFee: 50}}); err != nil {
		t.Fatal(err)
	}
	if _, ok, _ := MischiefTierByKey("boss"); ok {
		t.Error("a tier dropped from the push survived the replace")
	}
}

func TestUserEuroReplaceAndRead(t *testing.T) {
	setupTestDB(t)

	if err := ReplaceUserEuro([]MischiefBalance{{Username: "reala", Euro: 820.5}}, time.Now().Unix()); err != nil {
		t.Fatal(err)
	}
	euro, has, err := UserEuro("reala")
	if err != nil || !has || euro != 820.5 {
		t.Fatalf("UserEuro(reala) = %v has=%v err=%v", euro, has, err)
	}

	// A user gogobee has never reported reads as "unknown", not €0 — the
	// storefront shows no hint rather than a discouraging, wrong zero.
	if _, has, _ := UserEuro("stranger"); has {
		t.Error("UserEuro claimed to know a stranger's balance")
	}

	// Replace drops anyone the new snapshot omits.
	if err := ReplaceUserEuro([]MischiefBalance{{Username: "bob", Euro: 10}}, time.Now().Unix()); err != nil {
		t.Fatal(err)
	}
	if _, has, _ := UserEuro("reala"); has {
		t.Error("a balance that fell out of the snapshot survived the replace")
	}
}
