package plugin

import (
	"math/rand/v2"
	"strings"
	"testing"

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

// TestArchiveOrphanZoneRuns_LeavesLinkedRunsAlone verifies that a run
// pointed at by an active expedition.run_id survives the migration.
func TestArchiveOrphanZoneRuns_LeavesLinkedRunsAlone(t *testing.T) {
	setupZoneRunTestDB(t)
	uid := id.UserID("@r1-link-test:example.org")
	defer cleanupZoneRuns(uid)
	defer cleanupExpeditions(uid)

	rng := rand.New(rand.NewPCG(11, 7))
	run, err := startZoneRun(uid, ZoneGoblinWarrens, 1, rng)
	if err != nil {
		t.Fatalf("startZoneRun: %v", err)
	}
	exp, err := startExpedition(uid, ZoneGoblinWarrens, run.RunID, ExpeditionSupplies{Max: 5, Current: 5, DailyBurn: 1})
	if err != nil {
		t.Fatalf("startExpedition: %v", err)
	}
	_ = exp

	if _, err := archiveOrphanZoneRuns(); err != nil {
		t.Fatalf("archive: %v", err)
	}

	got, err := getZoneRun(run.RunID)
	if err != nil {
		t.Fatalf("getZoneRun: %v", err)
	}
	if got == nil || !got.IsActive() {
		t.Fatalf("linked run got archived; abandoned=%v", got != nil && !got.IsActive())
	}
}

// TestArchiveOrphanZoneRuns_RegionRunsPreserved verifies that runs only
// referenced via RegionState["region_runs"] (not exp.run_id) are kept.
func TestArchiveOrphanZoneRuns_RegionRunsPreserved(t *testing.T) {
	setupZoneRunTestDB(t)
	uid := id.UserID("@r1-region-test:example.org")
	defer cleanupZoneRuns(uid)
	defer cleanupExpeditions(uid)

	rng := rand.New(rand.NewPCG(13, 8))
	run, err := startZoneRun(uid, ZoneGoblinWarrens, 1, rng)
	if err != nil {
		t.Fatalf("startZoneRun: %v", err)
	}
	exp, err := startExpedition(uid, ZoneGoblinWarrens, "", ExpeditionSupplies{Max: 5, Current: 5, DailyBurn: 1})
	if err != nil {
		t.Fatalf("startExpedition: %v", err)
	}
	// Pin via region_runs only; exp.run_id stays empty.
	regionJSON := `{"region_runs":{"r1":"` + run.RunID + `"}}`
	if _, err := db.Get().Exec(`UPDATE dnd_expedition SET region_state = ? WHERE expedition_id = ?`, regionJSON, exp.ID); err != nil {
		t.Fatalf("set region_state: %v", err)
	}

	if _, err := archiveOrphanZoneRuns(); err != nil {
		t.Fatalf("archive: %v", err)
	}

	got, err := getZoneRun(run.RunID)
	if err != nil {
		t.Fatalf("getZoneRun: %v", err)
	}
	if got == nil || !got.IsActive() {
		t.Fatalf("region-linked run got archived")
	}
}

// TestArchiveOrphanZoneRuns_OrphanArchived verifies that an active zone
// run with no expedition pointing at it gets flagged abandoned.
func TestArchiveOrphanZoneRuns_OrphanArchived(t *testing.T) {
	setupZoneRunTestDB(t)
	uid := id.UserID("@r1-orphan-test:example.org")
	defer cleanupZoneRuns(uid)

	rng := rand.New(rand.NewPCG(17, 9))
	run, err := startZoneRun(uid, ZoneGoblinWarrens, 1, rng)
	if err != nil {
		t.Fatalf("startZoneRun: %v", err)
	}

	n, err := archiveOrphanZoneRuns()
	if err != nil {
		t.Fatalf("archive: %v", err)
	}
	if n < 1 {
		t.Fatalf("expected ≥1 archived row, got %d", n)
	}

	got, err := getZoneRun(run.RunID)
	if err != nil {
		t.Fatalf("getZoneRun: %v", err)
	}
	if got == nil {
		t.Fatal("run vanished")
	}
	if got.IsActive() {
		t.Fatal("orphan run still active after archive")
	}
}

// TestArchiveOrphanZoneRuns_Idempotent: a second call is a no-op.
func TestArchiveOrphanZoneRuns_Idempotent(t *testing.T) {
	setupZoneRunTestDB(t)
	uid := id.UserID("@r1-idempotent-test:example.org")
	defer cleanupZoneRuns(uid)

	rng := rand.New(rand.NewPCG(19, 11))
	if _, err := startZoneRun(uid, ZoneGoblinWarrens, 1, rng); err != nil {
		t.Fatalf("startZoneRun: %v", err)
	}
	if _, err := archiveOrphanZoneRuns(); err != nil {
		t.Fatalf("archive #1: %v", err)
	}
	n, err := archiveOrphanZoneRuns()
	if err != nil {
		t.Fatalf("archive #2: %v", err)
	}
	if n != 0 {
		t.Fatalf("expected 0 on second call, got %d", n)
	}
}

// TestIsLegacyActivityInput covers the deprecation-gate detection: legacy
// keywords / numbers match, unrelated DM input doesn't.
func TestIsLegacyActivityInput(t *testing.T) {
	cases := []struct {
		in   string
		want bool
	}{
		{"1", true},
		{"2 deep mine", true},
		{"dungeon", true},
		{"forage", true},
		{"fish", true},
		{"f", true},
		{"Soggy Cellar", true}, // location name match
		{"", false},
		{"hello there", false},
		{"7", false},
		{"!expedition", false},
	}
	for _, c := range cases {
		got := isLegacyActivityInput(strings.ToLower(c.in), nil)
		if got != c.want {
			t.Errorf("isLegacyActivityInput(%q) = %v, want %v", c.in, got, c.want)
		}
	}
}

// TestRenderLegacyActivityDeprecation: deprecation DM points at !expedition
// and the harvest entry-points so the user has somewhere to go.
func TestRenderLegacyActivityDeprecation(t *testing.T) {
	out := renderLegacyActivityDeprecation(nil)
	for _, want := range []string{"expedition", "Harvest is automatic", "retired"} {
		if !strings.Contains(out, want) {
			t.Errorf("deprecation DM missing %q; got:\n%s", want, out)
		}
	}
}
