package migration

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

	"reasonix/internal/config"
	"reasonix/internal/event"
)

const legacyMessageLog = `{"role":"user","content":"hello from v0.x"}
{"role":"assistant","content":"hi there"}
`

func migrationRescueHome(t *testing.T) string {
	t.Helper()
	home := t.TempDir()
	t.Setenv("HOME", home)
	t.Setenv("USERPROFILE", home)
	t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
	t.Setenv("AppData", filepath.Join(home, "AppData"))
	t.Setenv("REASONIX_HOME", "")
	t.Setenv("REASONIX_STATE_HOME", filepath.Join(home, "new-state"))
	t.Setenv("REASONIX_CREDENTIALS_STORE", "file")
	t.Chdir(t.TempDir())
	return home
}

func isolateMigrationHome(t *testing.T) string {
	t.Helper()
	home := migrationRescueHome(t)
	t.Setenv("REASONIX_HOME", filepath.Join(home, "new-reasonix"))
	t.Setenv("REASONIX_STATE_HOME", "")
	return home
}

func TestRunLegacyRescueImportsSessionsAndEmitsProgress(t *testing.T) {
	home := migrationRescueHome(t)
	legacyDir := filepath.Join(home, ".reasonix", "sessions")
	if err := os.MkdirAll(legacyDir, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(legacyDir, "old-chat.jsonl"), []byte(legacyMessageLog), 0o644); err != nil {
		t.Fatal(err)
	}

	var notices []string
	res := RunLegacyRescue(event.FuncSink(func(e event.Event) {
		if e.Kind == event.Notice {
			notices = append(notices, e.Text)
		}
	}))
	if res.ConfigErr != nil {
		t.Fatalf("config migration error: %v", res.ConfigErr)
	}
	if len(res.SessionErrs) != 0 {
		t.Fatalf("session migration errors: %v", res.SessionErrs)
	}
	if got := totalImported(res.SessionImports); got != 1 {
		t.Fatalf("imported sessions = %d, want 1; imports=%+v", got, res.SessionImports)
	}
	if _, err := os.Stat(filepath.Join(config.SessionDir(), "old-chat.jsonl")); err != nil {
		t.Fatalf("migrated session missing: %v", err)
	}
	joined := strings.Join(notices, "\n")
	for _, want := range []string{
		"migration rescue: checking legacy config and credentials",
		"migration rescue: scanning legacy sessions",
		"imported 1 past session(s)",
		"migration rescue complete: imported 1 past session(s)",
	} {
		if !strings.Contains(joined, want) {
			t.Fatalf("missing notice %q in:\n%s", want, joined)
		}
	}
}

func TestRunLegacyRescueImportsMemory(t *testing.T) {
	home := migrationRescueHome(t)
	legacyRoot := filepath.Join(home, ".reasonix")
	if err := os.MkdirAll(filepath.Join(legacyRoot, "memory", "global"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(legacyRoot, "REASONIX.md"), []byte("legacy user memory\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(legacyRoot, "memory", "global", "user.md"), []byte("---\nname: user\n---\nlegacy fact\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	projectMemory := filepath.Join(legacyRoot, "projects", "proj-slug", "memory")
	if err := os.MkdirAll(projectMemory, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(projectMemory, "project.md"), []byte("---\nname: project\n---\nproject fact\n"), 0o644); err != nil {
		t.Fatal(err)
	}

	var notices []string
	res := RunLegacyRescue(event.FuncSink(func(e event.Event) {
		if e.Kind == event.Notice {
			notices = append(notices, e.Text)
		}
	}))
	if len(res.MemoryErrs) != 0 {
		t.Fatalf("memory migration errors: %v", res.MemoryErrs)
	}
	if got := totalMemoryImported(res.MemoryImports); got != 3 {
		t.Fatalf("imported memory files = %d, want 3; imports=%+v", got, res.MemoryImports)
	}
	for _, path := range []string{
		filepath.Join(config.MemoryUserDir(), "REASONIX.md"),
		filepath.Join(config.MemoryUserDir(), "memory", "global", "user.md"),
		filepath.Join(config.MemoryUserDir(), "projects", "proj-slug", "memory", "project.md"),
	} {
		if _, err := os.Stat(path); err != nil {
			t.Fatalf("migrated memory missing at %s: %v", path, err)
		}
	}
	joined := strings.Join(notices, "\n")
	for _, want := range []string{
		"migration rescue: scanning legacy memory",
		"imported 3 memory file(s)",
		"migration rescue complete: imported 3 memory file(s)",
	} {
		if !strings.Contains(joined, want) {
			t.Fatalf("missing notice %q in:\n%s", want, joined)
		}
	}
}

func TestRunLegacyRescueNoopStillShowsProgress(t *testing.T) {
	migrationRescueHome(t)

	var notices []string
	res := RunLegacyRescue(event.FuncSink(func(e event.Event) {
		if e.Kind == event.Notice {
			notices = append(notices, e.Text)
		}
	}))
	if got := totalImported(res.SessionImports); got != 0 {
		t.Fatalf("imported sessions = %d, want 0", got)
	}
	joined := strings.Join(notices, "\n")
	for _, want := range []string{
		"migration rescue: checking legacy config and credentials",
		"migration rescue: no legacy sessions needed migration",
		"migration rescue complete: no legacy data needed migration",
	} {
		if !strings.Contains(joined, want) {
			t.Fatalf("missing notice %q in:\n%s", want, joined)
		}
	}
}

func TestRunLegacyRescueSkipsImplicitSourcesWhenIsolated(t *testing.T) {
	home := isolateMigrationHome(t)
	legacyRoot := filepath.Join(home, ".reasonix")
	if err := os.MkdirAll(filepath.Join(legacyRoot, "sessions"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(legacyRoot, "sessions", "old-chat.jsonl"), []byte(legacyMessageLog), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(legacyRoot, "REASONIX.md"), []byte("legacy user memory\n"), 0o644); err != nil {
		t.Fatal(err)
	}

	var notices []string
	res := RunLegacyRescue(event.FuncSink(func(e event.Event) {
		if e.Kind == event.Notice {
			notices = append(notices, e.Text)
		}
	}))
	if got := totalImported(res.SessionImports); got != 0 {
		t.Fatalf("imported sessions = %d, want 0; imports=%+v", got, res.SessionImports)
	}
	if got := totalMemoryImported(res.MemoryImports); got != 0 {
		t.Fatalf("imported memory files = %d, want 0; imports=%+v", got, res.MemoryImports)
	}
	if _, err := os.Stat(filepath.Join(config.SessionDir(), "old-chat.jsonl")); !os.IsNotExist(err) {
		t.Fatalf("isolated rescue imported legacy session, stat err=%v", err)
	}
	if _, err := os.Stat(filepath.Join(config.MemoryUserDir(), "REASONIX.md")); !os.IsNotExist(err) {
		t.Fatalf("isolated rescue imported legacy memory, stat err=%v", err)
	}
	joined := strings.Join(notices, "\n")
	if !strings.Contains(joined, "REASONIX_HOME is set; implicit legacy migration is skipped") {
		t.Fatalf("missing isolated skip notice in:\n%s", joined)
	}
}

func TestRunLegacyRescueCommandImportsFromExplicitInstallDir(t *testing.T) {
	home := isolateMigrationHome(t)
	installRoot := filepath.Join(home, "Custom Reasonix")
	legacySessions := filepath.Join(installRoot, "sessions")
	if err := os.MkdirAll(legacySessions, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(legacySessions, "custom-chat.jsonl"), []byte(legacyMessageLog), 0o644); err != nil {
		t.Fatal(err)
	}
	currentSessions := config.SessionDir()
	if err := os.MkdirAll(currentSessions, 0o755); err != nil {
		t.Fatal(err)
	}
	for _, marker := range []string{".legacy-imported.v2-routed", ".legacy-imported.v3-jsonl"} {
		if err := os.WriteFile(filepath.Join(currentSessions, marker), nil, 0o644); err != nil {
			t.Fatal(err)
		}
	}

	var notices []string
	res := RunLegacyRescueCommand(`--from "`+installRoot+`"`, event.FuncSink(func(e event.Event) {
		if e.Kind == event.Notice {
			notices = append(notices, e.Text)
		}
	}))
	if len(res.SessionErrs) != 0 {
		t.Fatalf("session migration errors: %v", res.SessionErrs)
	}
	if got := totalImported(res.SessionImports); got != 1 {
		t.Fatalf("imported sessions = %d, want 1; imports=%+v", got, res.SessionImports)
	}
	if _, err := os.Stat(filepath.Join(currentSessions, "custom-chat.jsonl")); err != nil {
		t.Fatalf("explicit imported session missing: %v", err)
	}
	joined := strings.Join(notices, "\n")
	for _, want := range []string{
		"migration rescue: scanning explicit legacy sessions from " + installRoot,
		"imported 1 past session(s) from " + legacySessions,
		"migration rescue complete: imported 1 past session(s)",
	} {
		if !strings.Contains(joined, want) {
			t.Fatalf("missing notice %q in:\n%s", want, joined)
		}
	}
}

func TestMigrateLegacySessionSourcesSkipsCurrentProjectTree(t *testing.T) {
	home := t.TempDir()
	t.Setenv("HOME", home)
	t.Setenv("USERPROFILE", home)
	t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
	t.Setenv("AppData", filepath.Join(home, "AppData"))
	t.Setenv("REASONIX_HOME", "")
	t.Setenv("REASONIX_STATE_HOME", "")
	if !samePath(config.MemoryUserDir(), filepath.Join(home, ".reasonix")) {
		t.Skip("current Reasonix home is not ~/.reasonix on this platform")
	}

	projectSessions := filepath.Join(config.MemoryUserDir(), "projects", "current-project", "sessions")
	subagents := filepath.Join(projectSessions, "subagents")
	if err := os.MkdirAll(subagents, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(subagents, "worker.jsonl"), []byte(legacyMessageLog), 0o644); err != nil {
		t.Fatal(err)
	}

	imports := MigrateLegacySessionSources(event.FuncSink(func(event.Event) {}))
	if got := totalImported(imports); got != 0 {
		t.Fatalf("imported sessions = %d, want 0; imports=%+v", got, imports)
	}
	if _, err := os.Stat(filepath.Join(projectSessions, "worker.jsonl")); !os.IsNotExist(err) {
		t.Fatalf("subagent transcript must not be copied into parent history, stat err=%v", err)
	}
}

func totalImported(imports []SessionImport) int {
	total := 0
	for _, imp := range imports {
		total += imp.Count
	}
	return total
}

func totalMemoryImported(imports []MemoryImport) int {
	total := 0
	for _, imp := range imports {
		total += imp.Count
	}
	return total
}
