package storage

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

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

	o, err := InsertEquipOrder("sub-1", "josie", "Josie", 42, "Cloak of Elvenkind", "cloak", EquipActionEquip)
	if err != nil {
		t.Fatal(err)
	}
	if o.Status != EquipPending {
		t.Fatalf("fresh order status = %q, want pending", o.Status)
	}
	if o.ItemID != 42 || o.Slot != "cloak" || o.Action != EquipActionEquip {
		t.Fatalf("order fields lost through insert: %+v", o)
	}

	pending, err := PendingEquipOrders(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)
	}

	got, err := ResolveEquipOrder(o.GUID, EquipApplied, "worn and bonded")
	if err != nil {
		t.Fatal(err)
	}
	if got.Status != EquipApplied || got.Detail != "worn and bonded" {
		t.Fatalf("resolved order = %+v, want applied with detail", got)
	}
	if pending, _ := PendingEquipOrders(10); len(pending) != 0 {
		t.Fatalf("applied order still pending: %+v", pending)
	}
}

// TestEquipResolveIsIdempotent: gogobee's poll loop retries, so a verdict can
// arrive twice — the second must not overwrite the first. This is the whole
// reason Pete can copy mischief's mechanic even though the game action underneath
// is not itself idempotent (gogobee guards that separately, on the order guid).
func TestEquipResolveIsIdempotent(t *testing.T) {
	setupTestDB(t)

	o, err := InsertEquipOrder("sub-1", "josie", "Josie", 7, "Ring of Protection", "ring_1", EquipActionEquip)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := ResolveEquipOrder(o.GUID, EquipApplied, "first"); err != nil {
		t.Fatal(err)
	}
	got, err := ResolveEquipOrder(o.GUID, EquipRejectedNotOwned, "second")
	if err != nil {
		t.Fatalf("re-resolve errored: %v", err)
	}
	if got.Status != EquipApplied || got.Detail != "first" {
		t.Fatalf("idempotency broken: order became %+v", got)
	}
}

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

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

	o, _ := InsertEquipOrder("sub-1", "josie", "Josie", 1, "Boots", "feet", EquipActionEquip)
	if _, err := ResolveEquipOrder(o.GUID, "exploded", ""); err == nil {
		t.Error("a bogus verdict status was accepted")
	}
	if got, _ := EquipOrderByGUID(o.GUID); got.Status != EquipPending {
		t.Fatalf("order moved off pending on a bad verdict: %q", got.Status)
	}
}

// TestEquipInsertRejectsBadAction: the action is part of the contract, so a value
// that isn't equip/unequip must not reach the table.
func TestEquipInsertRejectsBadAction(t *testing.T) {
	setupTestDB(t)
	if _, err := InsertEquipOrder("sub-1", "josie", "Josie", 1, "Thing", "cloak", "wield"); err == nil {
		t.Fatal("a bogus action was accepted")
	}
}

// TestEquipUnequipCarriesSlotNotItem: an unequip has no live inventory row to name,
// so it rides on the slot alone — item_id 0 is expected, not a bug.
func TestEquipUnequipCarriesSlotNotItem(t *testing.T) {
	setupTestDB(t)
	o, err := InsertEquipOrder("sub-1", "josie", "Josie", 0, "Cloak of Elvenkind", "cloak", EquipActionUnequip)
	if err != nil {
		t.Fatal(err)
	}
	got, _ := EquipOrderByGUID(o.GUID)
	if got.Action != EquipActionUnequip || got.Slot != "cloak" || got.ItemID != 0 {
		t.Fatalf("unequip order = %+v, want slot-keyed with no item id", got)
	}
}

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

	for i := 0; i < 3; i++ {
		if _, err := InsertEquipOrder("sub-A", "alice", "Alice", int64(i+1), "Item", "cloak", EquipActionEquip); err != nil {
			t.Fatal(err)
		}
	}
	if _, err := InsertEquipOrder("sub-B", "bob", "Bob", 9, "Item", "cloak", EquipActionEquip); err != nil {
		t.Fatal(err)
	}

	mine, err := EquipOrdersByOwner("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 := CountEquipOrdersSince("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, _ := CountEquipOrdersSince("sub-A", time.Now().Add(time.Hour).Unix()); n != 0 {
		t.Fatalf("count since the future = %d, want 0", n)
	}
}
