package storage

import "testing"

// The private self-detail set is the one place Pete holds a localpart↔token
// association. These tests pin the security contract of that store: the
// ownership join only ever unlocks a page for the localpart that owns it, and a
// replace wipes a departed player's stale self-view rather than leaving it to
// linger.

func seedSelfDetail(t *testing.T, localpart, token string) PlayerDetail {
	t.Helper()
	return PlayerDetail{
		Localpart: localpart,
		Token:     token,
		Inventory: []ItemView{{Name: "Iron Ore", Type: "ore", Tier: 1, Value: 10}},
		Vault:     []ItemView{{Name: "Jeweled Crown", Type: "treasure", Tier: 4, Value: 5000}},
		House:     HouseView{Tier: 2, LoanBalance: 1500},
		Pets:      []PetView{{Type: "cat", Name: "Mittens", Level: 3}},
	}
}

// TestPlayerDetailByOwnerMatrix is the ownership matrix: the self-view unlocks
// only when the signed-in localpart owns the exact page token. A different
// localpart, or the owner viewing someone else's page, gets nothing.
func TestPlayerDetailByOwnerMatrix(t *testing.T) {
	setupTestDB(t)
	if err := ReplacePlayerDetail([]PlayerDetail{
		seedSelfDetail(t, "josie", "tok-josie"),
		seedSelfDetail(t, "quack", "tok-quack"),
	}, 1000); err != nil {
		t.Fatalf("ReplacePlayerDetail: %v", err)
	}

	// The owner, on their own page: unlocked, and the private goods come through.
	pd, ok, err := PlayerDetailByOwner("josie", "tok-josie")
	if err != nil || !ok {
		t.Fatalf("owner on own page: ok=%v err=%v, want unlocked", ok, err)
	}
	if len(pd.Inventory) != 1 || pd.House.Tier != 2 || len(pd.Pets) != 1 {
		t.Errorf("owner detail = %+v, want inventory+house+pet carried", pd)
	}

	// Josie signed in, looking at Quack's page: her localpart doesn't own that
	// token, so the join must refuse — no peeking at another player's private set.
	if _, ok, _ := PlayerDetailByOwner("josie", "tok-quack"); ok {
		t.Error("owner unlocked ANOTHER player's page — the token guard failed")
	}

	// A signed-in stranger with no self-detail row at all.
	if _, ok, _ := PlayerDetailByOwner("nobody", "tok-josie"); ok {
		t.Error("a stranger unlocked a page they have no row for")
	}

	// Empty inputs never unlock.
	if _, ok, _ := PlayerDetailByOwner("", "tok-josie"); ok {
		t.Error("empty localpart unlocked a page")
	}
	if _, ok, _ := PlayerDetailByOwner("josie", ""); ok {
		t.Error("empty token unlocked a page")
	}
}

// TestReplacePlayerDetailReplaces: the set is swapped whole, never merged — a
// player gogobee stops pushing (deleted character, dropped out) must lose their
// stale self-view, the same complete-snapshot contract as the board.
func TestReplacePlayerDetailReplaces(t *testing.T) {
	setupTestDB(t)
	if err := ReplacePlayerDetail([]PlayerDetail{
		seedSelfDetail(t, "josie", "tok-josie"),
		seedSelfDetail(t, "quack", "tok-quack"),
	}, 1000); err != nil {
		t.Fatal(err)
	}
	// Next push carries only Josie; Quack has left.
	if err := ReplacePlayerDetail([]PlayerDetail{
		seedSelfDetail(t, "josie", "tok-josie"),
	}, 1060); err != nil {
		t.Fatal(err)
	}
	if _, ok, _ := PlayerDetailByOwner("quack", "tok-quack"); ok {
		t.Error("a dropped player's self-view survived the replace")
	}
	if _, ok, _ := PlayerDetailByOwner("josie", "tok-josie"); !ok {
		t.Error("the surviving player lost their self-view")
	}
}

// TestReplacePlayerDetailTokenFollows: when a player's board token rotates
// (re-derived each push), the ownership check must follow it. The stale token no
// longer unlocks; the current one does.
func TestReplacePlayerDetailTokenFollows(t *testing.T) {
	setupTestDB(t)
	if err := ReplacePlayerDetail([]PlayerDetail{seedSelfDetail(t, "josie", "tok-old")}, 1000); err != nil {
		t.Fatal(err)
	}
	if err := ReplacePlayerDetail([]PlayerDetail{seedSelfDetail(t, "josie", "tok-new")}, 1060); err != nil {
		t.Fatal(err)
	}
	if _, ok, _ := PlayerDetailByOwner("josie", "tok-old"); ok {
		t.Error("the stale token still unlocked the page after a rotation")
	}
	if _, ok, _ := PlayerDetailByOwner("josie", "tok-new"); !ok {
		t.Error("the current token failed to unlock the page")
	}
}

// TestReplacePlayerDetailSkipsUnusable: a row with no owner or no page to hang
// on is dropped at write time — it could never be served and would only be dead
// weight.
func TestReplacePlayerDetailSkipsUnusable(t *testing.T) {
	setupTestDB(t)
	if err := ReplacePlayerDetail([]PlayerDetail{
		{Localpart: "", Token: "tok-orphan"},
		{Localpart: "ghost", Token: ""},
		seedSelfDetail(t, "josie", "tok-josie"),
	}, 1000); err != nil {
		t.Fatal(err)
	}
	if tok, ok := SelfToken("josie"); !ok || tok != "tok-josie" {
		t.Errorf("SelfToken(josie) = %q,%v, want tok-josie", tok, ok)
	}
	if _, ok := SelfToken("ghost"); ok {
		t.Error("a tokenless row was stored")
	}
}
