package main

import (
	"context"
	"path/filepath"
	"testing"
	"time"

	"reasonix/internal/control"
	"reasonix/internal/repair"
)

type shutdownSnapshotController struct {
	control.SessionAPI
	calls           []string
	normalSnapshots int
	sessionPath     string
	shutdown        func() error
}

func (c *shutdownSnapshotController) Snapshot() error {
	c.normalSnapshots++
	return nil
}

func (c *shutdownSnapshotController) SnapshotForShutdown() error {
	c.calls = append(c.calls, "shutdown-snapshot")
	if c.shutdown != nil {
		return c.shutdown()
	}
	return nil
}

func (c *shutdownSnapshotController) SessionPath() string {
	if c.sessionPath != "" {
		return c.sessionPath
	}
	if c.SessionAPI != nil {
		return c.SessionAPI.SessionPath()
	}
	return ""
}

func (c *shutdownSnapshotController) Close() {
	c.calls = append(c.calls, "close")
	if c.SessionAPI != nil {
		c.SessionAPI.Close()
	}
}

func TestShutdownWaitsForRuntimeLifecycleMutation(t *testing.T) {
	isolateDesktopUserDirs(t)
	app := NewApp()
	app.runtimeAdmissionMu.Lock()
	admissionHeld := true
	defer func() {
		if admissionHeld {
			app.runtimeAdmissionMu.Unlock()
		}
	}()

	done := make(chan struct{})
	go func() {
		app.shutdown(context.Background())
		close(done)
	}()
	deadline := time.Now().Add(5 * time.Second)
	for app.runtimeRebuildMu.TryLock() {
		app.runtimeRebuildMu.Unlock()
		if time.Now().After(deadline) {
			t.Fatal("shutdown did not enter the runtime lifecycle barrier")
		}
		time.Sleep(time.Millisecond)
	}
	select {
	case <-done:
		t.Fatal("shutdown bypassed an in-flight runtime lifecycle mutation")
	default:
	}

	app.runtimeAdmissionMu.Unlock()
	admissionHeld = false
	select {
	case <-done:
	case <-time.After(5 * time.Second):
		t.Fatal("shutdown did not resume after the runtime lifecycle mutation completed")
	}
}

func TestShutdownDoesNotWaitForCancelledControllerBuild(t *testing.T) {
	isolateDesktopUserDirs(t)
	app := NewApp()
	tab := app.createTabEntryWithID("global", "", "", "blocked-build")
	app.mu.Lock()
	app.tabs[tab.ID] = tab
	app.tabOrder = []string{tab.ID}
	app.activeTabID = tab.ID
	app.mu.Unlock()

	started := make(chan struct{})
	release := make(chan struct{})
	app.tabBuildStartHook = func(string) {
		close(started)
		<-release
	}
	buildDone := make(chan struct{})
	go func() {
		app.startTabControllerBuild(tab)
		close(buildDone)
	}()
	<-started

	shutdownDone := make(chan struct{})
	go func() {
		app.shutdown(context.Background())
		close(shutdownDone)
	}()
	select {
	case <-shutdownDone:
		close(release)
	case <-time.After(750 * time.Millisecond):
		close(release)
		<-shutdownDone
		t.Fatal("shutdown waited for a cancelled controller build")
	}
	select {
	case <-buildDone:
	case <-time.After(5 * time.Second):
		t.Fatal("cancelled controller build did not finish")
	}
}

// TestShutdownRecordsLKGOnlyAfterReady pins that last-known-good config is only
// written after the window reached domReady. A quit before paint must not
// rewrite the LKG snapshot from an incomplete boot.
func TestShutdownRecordsLKGOnlyAfterReady(t *testing.T) {
	isolateDesktopUserDirs(t)
	a := NewApp()
	// Pre-ready shutdown is a no-op for LKG (startupReady is false).
	a.shutdown(context.Background())
	a.startupReady.Store(true)
	// Post-ready shutdown attempts RecordHealthyConfig; missing user config is fine.
	a.shutdown(context.Background())
}

func TestCaptureAndCommitPendingUpdateHealthUsesExactStartupIdentity(t *testing.T) {
	originalRead := readPendingUpdateForHealth
	originalMark := markPendingUpdateHealthyAfterReady
	t.Cleanup(func() {
		readPendingUpdateForHealth = originalRead
		markPendingUpdateHealthyAfterReady = originalMark
	})
	tx := &repair.UpdateTransaction{
		SchemaVersion: 1,
		ToVersion:     version,
		CreatedAt:     "2026-08-05T00:00:00Z",
		Platform:      "darwin/arm64",
		TargetKind:    "app-bundle",
		TargetPath:    "/Applications/Reasonix.app",
		BackupPath:    "/Applications/Reasonix.app.reasonix-update-backup",
	}
	readPendingUpdateForHealth = func() (*repair.UpdateTransaction, error) { return tx, nil }
	app := NewApp()
	capturePendingUpdateHealthIdentity(app)
	wantID := repair.UpdateTransactionID(tx)
	if app.healthyUpdateCreatedAt != tx.CreatedAt || app.healthyUpdateTransactionID != wantID {
		t.Fatalf("captured health identity=(%q,%q), want (%q,%q)", app.healthyUpdateCreatedAt, app.healthyUpdateTransactionID, tx.CreatedAt, wantID)
	}
	called := false
	markPendingUpdateHealthyAfterReady = func(running, createdAt, transactionID string) error {
		called = true
		if running != version || createdAt != tx.CreatedAt || transactionID != wantID {
			t.Fatalf("health commit=(%q,%q,%q)", running, createdAt, transactionID)
		}
		return nil
	}
	if err := app.commitPendingUpdateHealth(); err != nil {
		t.Fatal(err)
	}
	if !called {
		t.Fatal("exact startup transaction was not committed")
	}
}

func TestCapturePendingUpdateHealthAcceptsVersionPrefixMismatch(t *testing.T) {
	originalRead := readPendingUpdateForHealth
	originalVersion := version
	t.Cleanup(func() {
		readPendingUpdateForHealth = originalRead
		version = originalVersion
	})
	version = "1.21.0"
	readPendingUpdateForHealth = func() (*repair.UpdateTransaction, error) {
		return &repair.UpdateTransaction{
			ToVersion:  "v1.21.0",
			CreatedAt:  "2026-08-07T00:00:00Z",
			TargetKind: "file",
		}, nil
	}
	app := &App{}
	capturePendingUpdateHealthIdentity(app)
	if app.healthyUpdateCreatedAt == "" || app.healthyUpdateTransactionID == "" {
		t.Fatalf("expected health identity for v-prefix mismatch, got createdAt=%q id=%q",
			app.healthyUpdateCreatedAt, app.healthyUpdateTransactionID)
	}
}

func TestCapturePendingUpdateHealthRejectsDifferentTargetVersion(t *testing.T) {
	originalRead := readPendingUpdateForHealth
	t.Cleanup(func() { readPendingUpdateForHealth = originalRead })
	readPendingUpdateForHealth = func() (*repair.UpdateTransaction, error) {
		return &repair.UpdateTransaction{ToVersion: version + "-other", CreatedAt: "2026-08-05T00:00:00Z"}, nil
	}
	app := NewApp()
	capturePendingUpdateHealthIdentity(app)
	if app.healthyUpdateCreatedAt != "" || app.healthyUpdateTransactionID != "" {
		t.Fatalf("captured unrelated transaction: %+v", app)
	}
}

func TestShutdownUsesDurableSnapshotBeforeClosingController(t *testing.T) {
	isolateDesktopUserDirs(t)
	ctrl := &shutdownSnapshotController{SessionAPI: control.New(control.Options{Label: "shutdown"})}
	a := NewApp()
	a.tabs["tab"] = &WorkspaceTab{ID: "tab", Ctrl: ctrl}
	a.tabOrder = []string{"tab"}

	a.shutdown(context.Background())

	if ctrl.normalSnapshots != 0 {
		t.Fatalf("ordinary Snapshot calls = %d, want shutdown-specific persistence", ctrl.normalSnapshots)
	}
	if len(ctrl.calls) != 2 || ctrl.calls[0] != "shutdown-snapshot" || ctrl.calls[1] != "close" {
		t.Fatalf("shutdown call order = %v, want [shutdown-snapshot close]", ctrl.calls)
	}
}

func TestShutdownPersistsRecoveryPathCommittedAfterCallback(t *testing.T) {
	isolateDesktopUserDirs(t)
	dir := t.TempDir()
	originalPath := filepath.Join(dir, "original.jsonl")
	recoveryPath := filepath.Join(dir, "original-recovery.jsonl")
	a := NewApp()
	ctrl := &shutdownSnapshotController{
		SessionAPI:  control.New(control.Options{Label: "shutdown", SessionPath: originalPath}),
		sessionPath: originalPath,
	}
	tab := &WorkspaceTab{ID: "tab", Ctrl: ctrl, SessionPath: originalPath}
	a.tabs[tab.ID] = tab
	a.tabOrder = []string{tab.ID}
	a.activeTabID = tab.ID
	ctrl.shutdown = func() error {
		err := a.handleTabSessionRecovered(tab)(control.SessionRecoveryInfo{
			OriginalPath: originalPath,
			RecoveryPath: recoveryPath,
		})
		if err == nil {
			// Force a newer ordinary layout write while Controller still exposes
			// the old path. The recovery lease must keep this write anchored to
			// recovery instead of undoing the callback's first save.
			a.mu.Lock()
			a.saveTabsLocked()
			a.mu.Unlock()
			// Controller.commitRecoveredSession updates its path only after the
			// callback succeeds. Mirror that ordering exactly.
			ctrl.sessionPath = recoveryPath
		}
		return err
	}

	a.shutdown(context.Background())

	saved := loadTabsFile()
	if len(saved.Tabs) != 1 || saved.Tabs[0].ID != tab.ID {
		t.Fatalf("saved tabs = %+v, want recovered tab %q", saved.Tabs, tab.ID)
	}
	if got := saved.Tabs[0].SessionPath; got != recoveryPath {
		t.Fatalf("saved shutdown session path = %q, want recovery path %q", got, recoveryPath)
	}
	if len(ctrl.calls) != 2 || ctrl.calls[0] != "shutdown-snapshot" || ctrl.calls[1] != "close" {
		t.Fatalf("shutdown call order = %v, want [shutdown-snapshot close]", ctrl.calls)
	}
}
