import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, waitFor, cleanup, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import App from "../App";
import type { Session } from "../types";
import * as commands from "../lib/commands";

// ─── Module mocks ──────────────────────────────────────────────────────────────

vi.mock("@tauri-apps/api/app", () => ({
  getVersion: vi.fn().mockResolvedValue("0.0.0"),
}));

vi.mock("@tauri-apps/api/core", () => ({
  Channel: class {
    onmessage: ((event: unknown) => void) | null = null;
  },
}));

vi.mock("@tauri-apps/plugin-opener", () => ({
  openUrl: vi.fn(),
}));

vi.mock("@tauri-apps/plugin-dialog", () => ({
  open: vi.fn(),
}));

vi.mock("../lib/commands", () => ({
  listSessions: vi.fn(),
  listConfigs: vi.fn(),
  createSession: vi.fn(),
  approveSession: vi.fn(),
  discardSession: vi.fn(),
  getSession: vi.fn(),
  getSessionLog: vi.fn(),
  getSessionPlan: vi.fn(),
  getNewSessionHistorySummary: vi.fn().mockResolvedValue({ recentWorkingDirs: [] }),
  getNewSessionConfigDefaults: vi.fn().mockResolvedValue({
    steps: [],
    defaultSkippedSteps: [],
  }),
  listDirectory: vi.fn(),
  getUpdateReadiness: vi.fn(),
  cleanSessions: vi.fn(),
  deleteSession: vi.fn(),
  runSession: vi.fn(),
  cancelSession: vi.fn(),
  resetSession: vi.fn(),
  respondToOption: vi.fn(),
  runAllSessions: vi.fn(),
  fixSession: vi.fn(),
  askSession: vi.fn(),
  getAppConfig: vi.fn(),
  updateAppConfig: vi.fn(),
}));

vi.mock("../lib/updater", () => ({
  checkForUpdate: vi.fn().mockResolvedValue(null),
  downloadAndInstall: vi.fn(),
}));

vi.mock("../lib/desktopNotifications", () => ({
  notifyDesktop: vi.fn(),
}));

// ─── Helpers ──────────────────────────────────────────────────────────────────

function makeSession(overrides: Partial<Session> = {}): Session {
  return {
    id: "session-1",
    phase: "Planned",
    configSource: "default.yaml",
    baseDir: "/home/user/project",
    input: "test task",
    createdAt: "2026-01-01T00:00:00Z",
    workspaceMode: "Worktree",
    skippedSteps: [],
    ...overrides,
  };
}

/**
 * Set up runAllSessions mock to capture the channel so tests can emit events.
 * Returns control handles for firing each event at an explicit moment.
 */
function setupRunAllMock() {
  let capturedChannel: { onmessage: ((event: unknown) => void) | null } | null = null;
  let resolveRunAll!: () => void;
  let capturedTotal = 0;

  vi.mocked(commands.runAllSessions).mockImplementationOnce(
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (channel: any) => {
      capturedChannel = channel;
      return new Promise<void>((resolve) => {
        resolveRunAll = resolve;
      });
    },
  );

  return {
    /** Emit runAllStarted event. */
    emitStarted(total: number): void {
      capturedTotal = total;
      capturedChannel!.onmessage?.({
        event: "runAllStarted",
        data: { total },
      });
    },
    /** Emit runAllSessionStarted event. */
    emitSessionStarted(sessionId: string, input: string, total?: number): void {
      capturedChannel!.onmessage?.({
        event: "runAllSessionStarted",
        data: { sessionId, input, total: total ?? capturedTotal },
      });
    },
    /** Emit runAllSessionFinished event. */
    emitSessionFinished(
      sessionId: string,
      input: string,
      phase: Session["phase"],
      error?: string,
    ): void {
      capturedChannel!.onmessage?.({
        event: "runAllSessionFinished",
        data: { sessionId, input, phase, error },
      });
    },
    /** Emit runAllCompleted event. */
    emitCompleted(cancelled = 0): void {
      capturedChannel!.onmessage?.({
        event: "runAllCompleted",
        data: { cancelled },
      });
      resolveRunAll();
    },
  };
}

// ─── Tests ────────────────────────────────────────────────────────────────────

describe("App: Run All re-entry and state persistence", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.mocked(commands.listSessions).mockResolvedValue([]);
    vi.mocked(commands.listConfigs).mockResolvedValue([]);
    vi.mocked(commands.getSessionLog).mockResolvedValue("");
    vi.mocked(commands.getSessionPlan).mockResolvedValue("");
    vi.mocked(commands.listDirectory).mockResolvedValue([]);
    vi.mocked(commands.getUpdateReadiness).mockResolvedValue({ canAutoUpdate: true });
    vi.mocked(commands.cleanSessions).mockResolvedValue({ deleted: 0, skipped: 0 });
    vi.mocked(commands.getAppConfig).mockResolvedValue({ runAllParallelism: 1 });
    vi.mocked(commands.updateAppConfig).mockResolvedValue();
  });

  afterEach(() => {
    cleanup();
  });

  // --- Re-entry: navigation away and back ---

  it("returns to the same Run All progress view after navigating to a session", async () => {
    // Given: two planned sessions
    const sess1 = makeSession({ id: "s1", input: "task one" });
    const sess2 = makeSession({ id: "s2", input: "task two" });
    vi.mocked(commands.listSessions).mockResolvedValue([sess1, sess2]);
    const control = setupRunAllMock();

    render(<App />);
    await waitFor(() => screen.getByText("task one"));

    // Start Run All
    await userEvent.click(screen.getByText("Run All"));
    await act(async () => {
      control.emitStarted(2);
    });
    await act(async () => {
      control.emitSessionStarted("s1", "task one");
    });

    // Verify Run All is showing progress
    await waitFor(() => {
      expect(screen.getByText(/1 \/ 2 sessions/)).toBeInTheDocument();
    });

    // When: navigate to session detail
    await userEvent.click(screen.getByRole("button", { name: /task one/ }));

    // Then: Run All view is hidden (session detail is shown)
    await waitFor(() => {
      expect(screen.queryByText(/1 \/ 2 sessions/)).toBeNull();
    });

    // When: click Run All again (re-entry)
    await userEvent.click(screen.getByText("Run All"));

    // Then: progress is restored without a new runAllSessions call
    await waitFor(() => {
      expect(screen.getByText(/1 \/ 2 sessions/)).toBeInTheDocument();
    });
    // runAllSessions should still only have been called once
    expect(commands.runAllSessions).toHaveBeenCalledOnce();
  });

  it("does not call runAllSessions again on re-entry", async () => {
    // Given
    const sess1 = makeSession({ id: "s1", input: "task one" });
    vi.mocked(commands.listSessions).mockResolvedValue([sess1]);
    const control = setupRunAllMock();

    render(<App />);
    await waitFor(() => screen.getByText("task one"));

    // Start Run All
    await userEvent.click(screen.getByText("Run All"));
    await act(async () => {
      control.emitStarted(1);
      control.emitSessionStarted("s1", "task one");
    });

    // Navigate away then come back
    await userEvent.click(screen.getByRole("button", { name: /task one/ }));
    await userEvent.click(screen.getByText("Run All"));

    // Then: still only one call
    expect(commands.runAllSessions).toHaveBeenCalledOnce();
  });

  // --- State persistence through navigation ---

  it("preserves results when navigating away and back during execution", async () => {
    // Given
    const sess1 = makeSession({ id: "s1", input: "task one" });
    const sess2 = makeSession({ id: "s2", input: "task two" });
    vi.mocked(commands.listSessions).mockResolvedValue([sess1, sess2]);
    const control = setupRunAllMock();

    render(<App />);
    await waitFor(() => screen.getByText("task one"));

    await userEvent.click(screen.getByText("Run All"));
    await act(async () => {
      control.emitStarted(2);
      control.emitSessionStarted("s1", "task one");
      control.emitSessionFinished("s1", "task one", "Completed");
      control.emitSessionStarted("s2", "task two");
    });

    // Navigate away
    await userEvent.click(screen.getByRole("button", { name: /task one/ }));

    // Navigate back
    await userEvent.click(screen.getByText("Run All"));

    // Then: previously completed result is still visible
    await waitFor(() => {
      expect(screen.getByText(/2 \/ 2 sessions/)).toBeInTheDocument();
    });
    // The completed session result should be listed
    expect(screen.getAllByText("task one").length).toBeGreaterThan(0);
  });

  // --- Run All active button state ---

  it("shows Run All button as active (colored) during execution", async () => {
    // Given
    const sess1 = makeSession({ id: "s1", input: "task one" });
    vi.mocked(commands.listSessions).mockResolvedValue([sess1]);
    const control = setupRunAllMock();

    render(<App />);
    await waitFor(() => screen.getByText("task one"));

    // When: Run All is started
    await userEvent.click(screen.getByText("Run All"));
    await act(async () => {
      control.emitStarted(1);
    });

    // Then: Run All button has active styling
    const runAllButtons = screen.getAllByRole("button").filter(
      (btn) => btn.textContent === "Run All",
    );
    // The sidebar Run All button should have active styling
    const sidebarButton = runAllButtons[0];
    expect(sidebarButton.className).toContain("bg-blue-600");
  });

  it("enables Run All button during execution even when sessions transition to Running", async () => {
    // Given: sessions start as Planned, transition to Running after Run All starts
    const sess1 = makeSession({ id: "s1", input: "task one", phase: "Planned" });
    vi.mocked(commands.listSessions).mockResolvedValue([sess1]);
    const control = setupRunAllMock();

    render(<App />);
    await waitFor(() => screen.getByText("task one"));

    await userEvent.click(screen.getByText("Run All"));
    await act(async () => {
      control.emitStarted(1);
    });

    // When: sessions transition to Running (listSessions returns Running state)
    vi.mocked(commands.listSessions).mockResolvedValue([
      makeSession({ id: "s1", input: "task one", phase: "Running" }),
    ]);

    // Then: Run All button is still enabled (active state)
    // When runAllActive=true, aria-label becomes "View running sessions"
    const runAllButton = screen.getByRole("button", { name: "View running sessions" });
    expect(runAllButton).not.toBeDisabled();
  });

  // --- Completion and Done ---

  it("can return to completed results view after navigating away", async () => {
    // Given
    const sess1 = makeSession({ id: "s1", input: "task one" });
    vi.mocked(commands.listSessions).mockResolvedValue([sess1]);
    const control = setupRunAllMock();

    render(<App />);
    await waitFor(() => screen.getByText("task one"));

    await userEvent.click(screen.getByText("Run All"));
    await act(async () => {
      control.emitStarted(1);
      control.emitSessionStarted("s1", "task one");
      control.emitSessionFinished("s1", "task one", "Completed");
      control.emitCompleted();
    });

    // Navigate away
    await userEvent.click(screen.getByRole("button", { name: /task one/ }));
    await waitFor(() => {
      expect(screen.queryByText("Done")).toBeNull();
    });

    // When: click Run All to return to results
    await userEvent.click(screen.getByText("Run All"));

    // Then: completed results are shown and Done button is available
    await waitFor(() => {
      expect(screen.getByText("Done")).toBeInTheDocument();
    });
    expect(screen.getAllByText("Completed").length).toBeGreaterThan(0);
  });

  it("clears Run All state when Done is clicked, allowing fresh start", async () => {
    // Given: Run All has completed
    const sess1 = makeSession({ id: "s1", input: "task one" });
    vi.mocked(commands.listSessions).mockResolvedValue([sess1]);
    const control = setupRunAllMock();

    render(<App />);
    await waitFor(() => screen.getByText("task one"));

    await userEvent.click(screen.getByText("Run All"));
    await act(async () => {
      control.emitStarted(1);
      control.emitSessionStarted("s1", "task one");
      control.emitSessionFinished("s1", "task one", "Completed");
      control.emitCompleted();
    });

    // When: click Done
    await userEvent.click(screen.getByText("Done"));

    // Then: view returns to session list
    await waitFor(() => {
      expect(screen.queryByText("Done")).toBeNull();
    });

    // When: click Run All again — should start a new execution
    setupRunAllMock();
    await userEvent.click(screen.getByText("Run All"));

    // Then: runAllSessions is called again (second time)
    await waitFor(() => {
      expect(commands.runAllSessions).toHaveBeenCalledTimes(2);
    });
  });

  // --- Error handling ---

  it("shows error state and preserves it on re-entry", async () => {
    // Given
    const sess1 = makeSession({ id: "s1", input: "task one" });
    vi.mocked(commands.listSessions).mockResolvedValue([sess1]);

    vi.mocked(commands.runAllSessions).mockImplementationOnce(
      () => Promise.reject(new Error("backend crashed")),
    );

    render(<App />);
    await waitFor(() => screen.getByText("task one"));

    await userEvent.click(screen.getByText("Run All"));

    // Then: error is shown
    await waitFor(() => {
      expect(screen.getByText("Error")).toBeInTheDocument();
    });

    // Navigate away and come back
    await userEvent.click(screen.getByRole("button", { name: /task one/ }));
    await userEvent.click(screen.getByText("Run All"));

    // Then: error state is preserved on re-entry
    await waitFor(() => {
      expect(screen.getByText("Error")).toBeInTheDocument();
    });
  });

  // --- Cancelled state ---

  it("shows cancelled state and preserves it on re-entry", async () => {
    // Given
    const sess1 = makeSession({ id: "s1", input: "task one" });
    const sess2 = makeSession({ id: "s2", input: "task two" });
    vi.mocked(commands.listSessions).mockResolvedValue([sess1, sess2]);
    const control = setupRunAllMock();

    render(<App />);
    await waitFor(() => screen.getByText("task one"));

    await userEvent.click(screen.getByText("Run All"));
    await act(async () => {
      control.emitStarted(2);
      control.emitSessionStarted("s1", "task one");
      control.emitSessionFinished("s1", "task one", "Completed");
      // 1 cancelled
      control.emitCompleted(1);
    });

    // Then: cancelled state is shown
    await waitFor(() => {
      expect(screen.getByText("Cancelled")).toBeInTheDocument();
    });

    // Navigate away and back
    await userEvent.click(screen.getByRole("button", { name: /task one/ }));
    await userEvent.click(screen.getByText("Run All"));

    // Then: cancelled state persists
    await waitFor(() => {
      expect(screen.getByText("Cancelled")).toBeInTheDocument();
    });
  });

  // --- Sidebar refresh on Done ---

  it("triggers sidebar refresh when Done is clicked after completion", async () => {
    // Given
    const sess1 = makeSession({ id: "s1", input: "task one" });
    vi.mocked(commands.listSessions).mockResolvedValue([sess1]);
    const control = setupRunAllMock();

    render(<App />);
    await waitFor(() => {
      expect(commands.listSessions).toHaveBeenCalled();
    });

    const callsBefore = vi.mocked(commands.listSessions).mock.calls.length;

    await userEvent.click(screen.getByText("Run All"));
    await act(async () => {
      control.emitStarted(1);
      control.emitSessionStarted("s1", "task one");
      control.emitSessionFinished("s1", "task one", "Completed");
      control.emitCompleted();
    });

    // When: click Done
    await userEvent.click(screen.getByText("Done"));

    // Then: sidebar refreshes (listSessions called again)
    await waitFor(() => {
      expect(vi.mocked(commands.listSessions).mock.calls.length).toBeGreaterThan(callsBefore);
    });
  });
});
