package plugin

import (
	"io"
	"os"
	"path/filepath"
	"testing"

	"gogobee/internal/db"
	"maunium.net/go/mautrix/id"
)

// TestProdDB_DnDLayer exercises the D&D layer against a copy of the real
// prod database. Verifies that:
//   - Existing AdventureCharacter rows load cleanly
//   - ensureDnDCharacterForCombat creates a sensible auto-migrated sheet
//   - The auto-migrated row is well-formed (HP > 0, AC ≥ 10, level ≥ 1)
//   - !setup overwrite path works on auto-migrated chars
//   - HP persistence after combat doesn't corrupt the sheet
//
// Skips silently if the prod DB isn't present.
func TestProdDB_DnDLayer(t *testing.T) {
	src := "/home/reala-misaki/git/gogobee/data/gogobee.db"
	if _, err := os.Stat(src); err != nil {
		t.Skip("prod db not present at " + src)
	}

	dir := t.TempDir()
	dst := filepath.Join(dir, "gogobee.db")
	copyTestFile(t, src, dst)

	db.Close()
	if err := db.Init(dir); err != nil {
		t.Fatalf("Init: %v", err)
	}
	t.Cleanup(db.Close)

	// Mirror AdventurePlugin.Init: bootstrap legacy adventure_characters rows
	// into player_meta for any user_id not yet present. Required for DBs that
	// haven't gone through the L4-L5h dual-write soak (the L5 close-out
	// rewired loadAdvCharacter to source from player_meta only).
	bootstrapPlayerMetaFromLegacy()

	// 1. Load every adventure character. They must scan without error.
	chars, err := loadAllAdvCharacters()
	if err != nil {
		t.Fatalf("loadAllAdvCharacters: %v", err)
	}
	if len(chars) == 0 {
		t.Fatal("no adventure characters in prod DB")
	}
	t.Logf("loaded %d real adventure characters", len(chars))

	// 2. For each, verify they have NO dnd_character row yet, then auto-migrate
	//    and verify the resulting row is sensible. Track which chars we
	//    actually migrated so steps 3+ can target one of those.
	migrated := make(map[id.UserID]bool)
	for i := range chars {
		char := &chars[i]
		uid := char.UserID

		existing, err := LoadDnDCharacter(uid)
		if err != nil {
			t.Errorf("user=%s: LoadDnDCharacter pre-migrate failed: %v", uid, err)
			continue
		}
		if existing != nil {
			// Real prod DB has accumulated rows from live bot use.
			// Skip these — the auto-migrate path is exercised by
			// other characters in the loop.
			t.Logf("user=%s: skipping (already has dnd_character row)", uid)
			continue
		}

		// Auto-migrate via the production code path.
		dnd, fresh, err := ensureDnDCharacterForCombat(uid, char)
		if err != nil {
			t.Errorf("user=%s: ensureDnDCharacterForCombat failed: %v", uid, err)
			continue
		}
		if dnd == nil {
			t.Errorf("user=%s: ensureDnDCharacterForCombat returned nil", uid)
			continue
		}
		if !fresh {
			t.Errorf("user=%s: expected fresh=true on first auto-migrate", uid)
		}

		// Sanity invariants on the auto-migrated sheet.
		if dnd.UserID != uid {
			t.Errorf("user=%s: dnd.UserID=%s mismatch", uid, dnd.UserID)
		}
		if !dnd.AutoMigrated {
			t.Errorf("user=%s: AutoMigrated=false on fresh auto-migration", uid)
		}
		if dnd.PendingSetup {
			t.Errorf("user=%s: PendingSetup=true on fresh auto-migration", uid)
		}
		if dnd.Race == "" || dnd.Class == "" {
			t.Errorf("user=%s: race/class empty: race=%q class=%q", uid, dnd.Race, dnd.Class)
		}
		if dnd.Level < 1 {
			t.Errorf("user=%s: level %d < 1", uid, dnd.Level)
		}
		// Migrated level = combat_level/5 clamped to [1, 20].
		expectLevel := dndLevelFromCombatLevel(char.CombatLevel)
		if dnd.Level != expectLevel {
			t.Errorf("user=%s: dnd.Level=%d, want %d (from combat_level=%d)",
				uid, dnd.Level, expectLevel, char.CombatLevel)
		}
		if dnd.HPMax < 1 || dnd.HPCurrent != dnd.HPMax {
			t.Errorf("user=%s: HP invariant: max=%d cur=%d", uid, dnd.HPMax, dnd.HPCurrent)
		}
		if dnd.ArmorClass < 10 {
			t.Errorf("user=%s: AC %d < 10 floor", uid, dnd.ArmorClass)
		}
		// Stat scores after racial mods should still be in legal range (1..20).
		for j, score := range []int{dnd.STR, dnd.DEX, dnd.CON, dnd.INT, dnd.WIS, dnd.CHA} {
			if score < 1 || score > 20 {
				t.Errorf("user=%s: stat[%d]=%d out of range", uid, j, score)
			}
		}

		migrated[uid] = true
		t.Logf("auto-migrated user=%s → L%d %s %s  HP=%d  AC=%d  STR/DEX/CON/INT/WIS/CHA=%d/%d/%d/%d/%d/%d",
			uid, dnd.Level, dnd.Race, dnd.Class, dnd.HPMax, dnd.ArmorClass,
			dnd.STR, dnd.DEX, dnd.CON, dnd.INT, dnd.WIS, dnd.CHA)
	}

	if len(migrated) == 0 {
		t.Skip("no characters available for auto-migrate (all pre-existing); steps 3+ require a fresh migrate")
	}

	// 3. Re-load each auto-migrated char — round-trip integrity.
	for i := range chars {
		uid := chars[i].UserID
		if !migrated[uid] {
			continue
		}
		dnd, err := LoadDnDCharacter(uid)
		if err != nil {
			t.Errorf("user=%s: re-load failed: %v", uid, err)
			continue
		}
		if dnd == nil {
			t.Errorf("user=%s: re-load returned nil (auto-migrate didn't persist)", uid)
			continue
		}
		if !dnd.AutoMigrated || dnd.PendingSetup {
			t.Errorf("user=%s: post-reload flags wrong: auto=%v pending=%v",
				uid, dnd.AutoMigrated, dnd.PendingSetup)
		}
	}

	// 4. ensureDnDCharacterForCombat is idempotent — second call returns the
	//    same row, doesn't create a duplicate. Pick the first migrated char.
	var idemIdx int
	for i := range chars {
		if migrated[chars[i].UserID] {
			idemIdx = i
			break
		}
	}
	uid := chars[idemIdx].UserID
	first, _ := LoadDnDCharacter(uid)
	second, freshAgain, err := ensureDnDCharacterForCombat(uid, &chars[idemIdx])
	if err != nil {
		t.Errorf("idempotent ensure failed: %v", err)
	}
	if freshAgain {
		t.Error("second ensure call returned fresh=true; should be false (already migrated)")
	}
	if first != nil && second != nil {
		if first.Race != second.Race || first.Class != second.Class {
			t.Errorf("ensure returned different sheet on second call: %v vs %v",
				first, second)
		}
	}

	// 5. HP persistence smoke test — combat is now on the dnd HP scale, so
	//    endHP is copied directly into hp_current (clamped to [0, hp_max]).
	pre, _ := LoadDnDCharacter(uid)
	endHP := pre.HPMax / 2
	persistDnDHPAfterCombat(uid, endHP)
	after, err := LoadDnDCharacter(uid)
	if err != nil || after == nil {
		t.Fatalf("post-persist load: %v", err)
	}
	if after.HPCurrent != endHP {
		t.Errorf("hp_current=%d after persist(endHP=%d); want exact copy (hp_max=%d)",
			after.HPCurrent, endHP, after.HPMax)
	}
	t.Logf("HP persistence verified: %d/%d after combat", after.HPCurrent, after.HPMax)

	// 6. !setup overwrite path: an auto-migrated char should be wipeable via
	//    loadOrInitDraft → DELETE → fresh draft.
	draft, err := loadOrInitDraft(uid)
	if err != nil {
		t.Fatalf("loadOrInitDraft on auto-migrated: %v", err)
	}
	if !draft.PendingSetup {
		t.Error("draft should have PendingSetup=true")
	}
	// The auto-migrated row should now be deleted.
	cleared, _ := LoadDnDCharacter(uid)
	if cleared != nil {
		t.Errorf("auto-migrated row not cleared by loadOrInitDraft: %+v", cleared)
	}

	// 7. Adventure characters list should still be loadable without
	//    interference from D&D-layer joins.
	chars2, err := loadAllAdvCharacters()
	if err != nil {
		t.Fatalf("post-test reload failed: %v", err)
	}
	if len(chars2) != len(chars) {
		t.Errorf("char count drift: pre=%d post=%d", len(chars), len(chars2))
	}
}

func copyTestFile(t *testing.T, src, dst string) {
	t.Helper()
	in, err := os.Open(src)
	if err != nil {
		t.Fatal(err)
	}
	defer in.Close()
	out, err := os.Create(dst)
	if err != nil {
		t.Fatal(err)
	}
	defer out.Close()
	if _, err := io.Copy(out, in); err != nil {
		t.Fatal(err)
	}
}

// silence unused warning if id pkg drifts
var _ = id.UserID("")
