package storage

import "testing"

// adventure_events is the only adventure store that accumulates, so it is the
// only one where a duplicate delivery is permanently wrong: the roster forgives
// a double push by replacing itself, a double-counted boss kill is in the tally
// forever. These tests pin that, and pin what the trophy case will and won't
// credit an adventurer for.

func ev(guid, typ, subject string, at int64) *AdvEvent {
	return &AdvEvent{GUID: guid, EventType: typ, Subject: subject, OccurredAt: at}
}

// TestInsertAdventureEventIdempotent: gogobee retries a fact whose ack it lost.
// The retry must be a no-op, not a second kill.
func TestInsertAdventureEventIdempotent(t *testing.T) {
	setupTestDB(t)
	e := ev("boss_kill:abc:1", "boss_kill", "Josie", 1000)
	e.Boss = "The Rotmother"

	for i := 0; i < 3; i++ {
		if err := InsertAdventureEvent(e); err != nil {
			t.Fatalf("insert %d: %v", i, err)
		}
	}
	events, err := EventsBySubject("Josie", 0)
	if err != nil {
		t.Fatalf("EventsBySubject: %v", err)
	}
	if len(events) != 1 {
		t.Fatalf("after 3 deliveries of one fact: got %d rows, want 1", len(events))
	}
	if tc := BuildTrophyCase("Josie", events); tc.BossKills != 1 {
		t.Fatalf("boss kills: got %d, want 1 — a retry inflated the tally", tc.BossKills)
	}
}

// TestTrophyCaseCounts walks a full history and checks each tally.
func TestTrophyCaseCounts(t *testing.T) {
	setupTestDB(t)

	first := ev("boss_first:1:1", "boss_first", "Josie", 100)
	first.Boss = "The Rotmother"
	repeat := ev("boss_kill:2:2", "boss_kill", "Josie", 200)
	repeat.Boss = "The Rotmother"
	other := ev("boss_kill:3:3", "boss_kill", "Josie", 300)
	other.Boss = "Gravebloom"
	zone := ev("zone_clear:4:4", "zone_clear", "Josie", 400)
	zone.Zone, zone.Region = "holymachina", "the Reach"
	death := ev("death:5:5", "death", "Josie", 500)
	mile := ev("milestone:6:6", "milestone", "Josie", 600)
	mile.Milestone = "level 20"

	for _, e := range []*AdvEvent{first, repeat, other, zone, death, mile} {
		if err := InsertAdventureEvent(e); err != nil {
			t.Fatalf("insert %s: %v", e.GUID, err)
		}
	}

	events, err := EventsBySubject("Josie", 0)
	if err != nil {
		t.Fatalf("EventsBySubject: %v", err)
	}
	tc := BuildTrophyCase("Josie", events)

	if tc.BossKills != 3 {
		t.Errorf("boss kills: got %d, want 3 (a first is still a kill)", tc.BossKills)
	}
	if tc.BossFirsts != 1 {
		t.Errorf("boss firsts: got %d, want 1", tc.BossFirsts)
	}
	if tc.ZoneClears != 1 || tc.Deaths != 1 {
		t.Errorf("zones/deaths: got %d/%d, want 1/1", tc.ZoneClears, tc.Deaths)
	}
	if len(tc.Milestones) != 1 || tc.Milestones[0] != "level 20" {
		t.Errorf("milestones: got %v, want [level 20]", tc.Milestones)
	}
	// Ordering is by kills desc: the twice-killed Rotmother outranks Gravebloom.
	if len(tc.Bosses) != 2 || tc.Bosses[0].Boss != "The Rotmother" || tc.Bosses[0].Kills != 2 {
		t.Fatalf("boss tallies: got %+v, want Rotmother×2 first", tc.Bosses)
	}
	if !tc.Bosses[0].First {
		t.Error("Rotmother should be flagged as a realm-first")
	}
	if tc.FirstSeen != 100 || tc.LastSeen != 600 {
		t.Errorf("span: got %d..%d, want 100..600", tc.FirstSeen, tc.LastSeen)
	}
}

// TestTrophyCaseTreasures: story-grade finds count out of the treasure_found
// fact, the priority tier marks a realm-first hoard, and the item name rides the
// stakes field into a per-find row. A find with no name still counts but earns no
// showcase row.
func TestTrophyCaseTreasures(t *testing.T) {
	setupTestDB(t)

	hoard := ev("treasure_found:1:1", "treasure_found", "Josie", 100)
	hoard.Tier, hoard.Stakes, hoard.Zone = "priority", "Crown of the Drowned King", "The Ossuary"
	find := ev("treasure_found:2:2", "treasure_found", "Josie", 200)
	find.Tier, find.Stakes, find.Zone = "bulletin", "Ring of Nine Sorrows", "The Sump"
	nameless := ev("treasure_found:3:3", "treasure_found", "Josie", 300)
	nameless.Tier = "bulletin" // a find gogobee sent without a stakes noun

	for _, e := range []*AdvEvent{hoard, find, nameless} {
		if err := InsertAdventureEvent(e); err != nil {
			t.Fatalf("insert %s: %v", e.GUID, err)
		}
	}

	events, err := EventsBySubject("Josie", 0)
	if err != nil {
		t.Fatalf("EventsBySubject: %v", err)
	}
	tc := BuildTrophyCase("Josie", events)

	if tc.TreasuresFound != 3 {
		t.Errorf("treasures found: got %d, want 3 (a nameless find still happened)", tc.TreasuresFound)
	}
	if tc.TreasureFirsts != 1 {
		t.Errorf("treasure firsts: got %d, want 1 (only the priority hoard)", tc.TreasureFirsts)
	}
	// Only the two named finds earn a showcase row, newest first.
	if len(tc.Treasures) != 2 {
		t.Fatalf("treasure rows: got %d, want 2 (nameless earns no row)", len(tc.Treasures))
	}
	if tc.Treasures[0].Item != "Ring of Nine Sorrows" || tc.Treasures[0].First {
		t.Errorf("newest row wrong: %+v", tc.Treasures[0])
	}
	if tc.Treasures[1].Item != "Crown of the Drowned King" || !tc.Treasures[1].First || tc.Treasures[1].Zone != "The Ossuary" {
		t.Errorf("hoard row wrong: %+v", tc.Treasures[1])
	}
}

// TestTrophyCaseIgnoresOpponentCredit is the one that matters for honesty. A
// duel Josie *lost* still names her, as the opponent in Quack's dispatch. It
// belongs on her timeline but must never be credited to her trophy case.
func TestTrophyCaseIgnoresOpponentCredit(t *testing.T) {
	setupTestDB(t)

	lost := ev("rival_result:1:1", "rival_result", "Quack", 100)
	lost.Opponent = "Josie" // Quack won; Josie is the one who got beaten
	won := ev("rival_result:2:2", "rival_result", "Josie", 200)
	won.Opponent = "Quack"

	for _, e := range []*AdvEvent{lost, won} {
		if err := InsertAdventureEvent(e); err != nil {
			t.Fatalf("insert: %v", err)
		}
	}

	events, err := EventsBySubject("Josie", 0)
	if err != nil {
		t.Fatalf("EventsBySubject: %v", err)
	}
	// Both facts are hers to *see* — the loss is part of her story.
	if len(events) != 2 {
		t.Fatalf("timeline: got %d events, want 2 (a loss is still history)", len(events))
	}
	// But only the one she won is hers to *claim*.
	if tc := BuildTrophyCase("Josie", events); tc.RivalWins != 1 {
		t.Fatalf("rival wins: got %d, want 1 — a loss was credited as a win", tc.RivalWins)
	}
}
