/**
 * @file Tests for DestructiveLaneModal: the preflight-gated confirmation for
 * reset/remove/purge. Covers that the displayed counts are exactly the
 * preflight facts, that `reset` is refused for adopted/missing/unreadable
 * lanes while `remove` stays available for all of them (the server permits it,
 * so the UI must not be stricter), that the Force checkbox appears exactly when
 * the server would demand force, and that confirming echoes back exactly the
 * `expect` block the modal displayed.
 * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
 */

import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";

const { preflightMock } = vi.hoisted(() => ({ preflightMock: vi.fn() }));
vi.mock("../../../lib/api", () => ({
  api: { lanes: { preflight: preflightMock } },
}));

import { DestructiveLaneModal } from "../DestructiveLaneModal";
import type { Lane, LanePurgePreflight, LaneWorktreePreflight } from "../../../lib/types";

function makeLane(overrides: Partial<Lane> = {}): Lane {
  // `as Lane`: spreading a Partial<Lane> widens every field it may carry to
  // `T | undefined`, which no longer satisfies Lane's required fields. The
  // base object below still lists all of them, so the cast asserts what the
  // literal already proves.
  return {
    id: 1,
    title: "demo",
    cwd: "/work/demo",
    branch: "lane/demo",
    kind: "managed",
    pipeline: "default",
    session_id: null,
    run_id: null,
    stage: "plan",
    stage_since: null,
    status: "idle",
    gate_decision: null,
    ci_status: null,
    needs_action: null,
    links: {},
    stages: {},
    notes: null,
    pipeline_name: "Default",
    pipeline_nodes: [],
    progress: 0,
    stage_seconds: null,
    last_event_seconds: null,
    liveness: "idle",
    detected_stage: null,
    detected_signal: null,
    slot: null,
    ports: {},
    ...overrides,
  } as Lane;
}

function worktreePreflight(overrides: Partial<LaneWorktreePreflight> = {}): LaneWorktreePreflight {
  return {
    action: "reset",
    lane: 1,
    kind: "managed",
    branch: "lane/demo",
    head: "abc1234",
    dirty: 2,
    untracked: 3,
    unpushed: 0,
    blocked: [],
    warnings: [],
    ...overrides,
  };
}

beforeEach(() => {
  preflightMock.mockReset();
});

describe("DestructiveLaneModal", () => {
  it("renders exactly the counts the preflight returned", async () => {
    preflightMock.mockResolvedValue(
      worktreePreflight({ head: "deadbee", dirty: 4, untracked: 5, unpushed: 0 })
    );
    render(
      <DestructiveLaneModal
        lane={makeLane()}
        action="reset"
        open
        onClose={vi.fn()}
        onConfirm={vi.fn()}
      />
    );
    expect(await screen.findByText("deadbee")).toBeInTheDocument();
    expect(screen.getByText("4")).toBeInTheDocument();
    expect(screen.getByText("5")).toBeInTheDocument();
  });

  it("disables RESET for an adopted lane (a worktree action can never touch it)", async () => {
    preflightMock.mockResolvedValue(
      worktreePreflight({ action: "reset", kind: "adopted", blocked: ["adopted"] })
    );
    render(
      <DestructiveLaneModal
        lane={makeLane({ kind: "adopted" })}
        action="reset"
        open
        onClose={vi.fn()}
        onConfirm={vi.fn()}
      />
    );
    const confirmButton = await screen.findByRole("button", { name: "Reset worktree" });
    await waitFor(() => expect(confirmButton).toBeDisabled());
  });

  it("ENABLES remove for an adopted lane and sends the payload — the server forgets the row and leaves the directory alone", async () => {
    preflightMock.mockResolvedValue(
      worktreePreflight({
        action: "remove",
        kind: "adopted",
        head: "adopt01",
        dirty: 0,
        untracked: 0,
        unpushed: 0,
        blocked: ["adopted"],
      })
    );
    const onConfirm = vi.fn();
    render(
      <DestructiveLaneModal
        lane={makeLane({ kind: "adopted" })}
        action="remove"
        open
        onClose={vi.fn()}
        onConfirm={onConfirm}
      />
    );
    const confirmButton = await screen.findByRole("button", { name: "Remove lane" });
    await waitFor(() => expect(confirmButton).not.toBeDisabled());
    // "adopted" is shown as context, not as a refusal.
    expect(screen.getByText(/Only the dashboard's record of it is dropped/)).toBeInTheDocument();
    fireEvent.click(confirmButton);
    expect(onConfirm).toHaveBeenCalledWith({
      expect: { head: "adopt01", dirty: 0, untracked: 0, unpushed: 0 },
    });
  });

  it("forgets an adopted lane with unpushed commits without offering Force — the server does not require it", async () => {
    preflightMock.mockResolvedValue(
      worktreePreflight({
        action: "remove",
        kind: "adopted",
        head: "adopt02",
        dirty: 0,
        untracked: 0,
        unpushed: 7,
        blocked: ["adopted", "unpushed-commits"],
      })
    );
    const onConfirm = vi.fn();
    render(
      <DestructiveLaneModal
        lane={makeLane({ kind: "adopted" })}
        action="remove"
        open
        onClose={vi.fn()}
        onConfirm={onConfirm}
      />
    );
    const confirmButton = await screen.findByRole("button", { name: "Remove lane" });
    await waitFor(() => expect(confirmButton).not.toBeDisabled());
    expect(screen.queryByRole("checkbox")).not.toBeInTheDocument();
    fireEvent.click(confirmButton);
    expect(onConfirm).toHaveBeenCalledWith({
      expect: { head: "adopt02", dirty: 0, untracked: 0, unpushed: 7 },
    });
  });

  it("disables RESET when the worktree directory is missing", async () => {
    preflightMock.mockResolvedValue(worktreePreflight({ action: "reset", blocked: ["missing"] }));
    render(
      <DestructiveLaneModal
        lane={makeLane()}
        action="reset"
        open
        onClose={vi.fn()}
        onConfirm={vi.fn()}
      />
    );
    const confirmButton = await screen.findByRole("button", { name: "Reset worktree" });
    await waitFor(() => expect(confirmButton).toBeDisabled());
  });

  it("ENABLES remove when the worktree directory is missing — the server takes the prune path", async () => {
    preflightMock.mockResolvedValue(
      worktreePreflight({
        action: "remove",
        head: null,
        dirty: 0,
        untracked: 0,
        unpushed: 0,
        blocked: ["missing"],
      })
    );
    const onConfirm = vi.fn();
    render(
      <DestructiveLaneModal
        lane={makeLane()}
        action="remove"
        open
        onClose={vi.fn()}
        onConfirm={onConfirm}
      />
    );
    const confirmButton = await screen.findByRole("button", { name: "Remove lane" });
    await waitFor(() => expect(confirmButton).not.toBeDisabled());
    expect(screen.getByText(/The lane directory is already gone/)).toBeInTheDocument();
    fireEvent.click(confirmButton);
    expect(onConfirm).toHaveBeenCalledWith({
      expect: { head: null, dirty: 0, untracked: 0, unpushed: 0 },
    });
  });

  it("disables RESET when the worktree directory is unreadable", async () => {
    preflightMock.mockResolvedValue(
      worktreePreflight({ action: "reset", blocked: ["unreadable"] })
    );
    render(
      <DestructiveLaneModal
        lane={makeLane()}
        action="reset"
        open
        onClose={vi.fn()}
        onConfirm={vi.fn()}
      />
    );
    const confirmButton = await screen.findByRole("button", { name: "Reset worktree" });
    await waitFor(() => expect(confirmButton).toBeDisabled());
  });

  it("ENABLES remove when the worktree directory is unreadable — the server attempts removal and falls back to deregistering it", async () => {
    preflightMock.mockResolvedValue(
      worktreePreflight({
        action: "remove",
        head: null,
        dirty: 0,
        untracked: 0,
        unpushed: 0,
        blocked: ["unreadable"],
      })
    );
    const onConfirm = vi.fn();
    render(
      <DestructiveLaneModal
        lane={makeLane()}
        action="remove"
        open
        onClose={vi.fn()}
        onConfirm={onConfirm}
      />
    );
    const confirmButton = await screen.findByRole("button", { name: "Remove lane" });
    await waitFor(() => expect(confirmButton).not.toBeDisabled());
    expect(screen.getByText(/cannot be read as a Git worktree/)).toBeInTheDocument();
    fireEvent.click(confirmButton);
    expect(onConfirm).toHaveBeenCalledWith({
      expect: { head: null, dirty: 0, untracked: 0, unpushed: 0 },
    });
  });

  it("shows the Force checkbox only when the sole blocker is unpushed commits, and ticking it enables confirm", async () => {
    preflightMock.mockResolvedValue(
      worktreePreflight({ action: "remove", unpushed: 3, blocked: ["unpushed-commits"] })
    );
    render(
      <DestructiveLaneModal
        lane={makeLane()}
        action="remove"
        open
        onClose={vi.fn()}
        onConfirm={vi.fn()}
      />
    );
    const confirmButton = await screen.findByRole("button", { name: "Remove lane" });
    await waitFor(() => expect(confirmButton).toBeDisabled());
    fireEvent.click(screen.getByRole("checkbox"));
    expect(confirmButton).not.toBeDisabled();
  });

  it("a local-only managed lane (no-remote warning, unpushed-commits blocker) is resettable with Force", async () => {
    preflightMock.mockResolvedValue(
      worktreePreflight({
        action: "reset",
        unpushed: 3,
        blocked: ["unpushed-commits"],
        warnings: ["no-remote"],
      })
    );
    render(
      <DestructiveLaneModal
        lane={makeLane()}
        action="reset"
        open
        onClose={vi.fn()}
        onConfirm={vi.fn()}
      />
    );
    // The warning is shown as context, not as an obstacle.
    expect(await screen.findByText(/No Git remote is configured/)).toBeInTheDocument();
    const confirmButton = await screen.findByRole("button", { name: "Reset worktree" });
    await waitFor(() => expect(confirmButton).toBeDisabled());
    fireEvent.click(screen.getByRole("checkbox"));
    expect(confirmButton).not.toBeDisabled();
  });

  it("does not offer the Force checkbox when a hard blocker makes confirming impossible anyway", async () => {
    preflightMock.mockResolvedValue(
      worktreePreflight({ action: "reset", unpushed: 3, blocked: ["unpushed-commits", "missing"] })
    );
    render(
      <DestructiveLaneModal
        lane={makeLane()}
        action="reset"
        open
        onClose={vi.fn()}
        onConfirm={vi.fn()}
      />
    );
    const confirmButton = await screen.findByRole("button", { name: "Reset worktree" });
    await waitFor(() => expect(confirmButton).toBeDisabled());
    expect(screen.queryByRole("checkbox")).not.toBeInTheDocument();
  });

  it("confirming a worktree action passes back exactly the expect block that was displayed", async () => {
    preflightMock.mockResolvedValue(
      worktreePreflight({ head: "cafefeed", dirty: 1, untracked: 2, unpushed: 0, blocked: [] })
    );
    const onConfirm = vi.fn();
    render(
      <DestructiveLaneModal
        lane={makeLane()}
        action="reset"
        open
        onClose={vi.fn()}
        onConfirm={onConfirm}
      />
    );
    const confirmButton = await screen.findByRole("button", { name: "Reset worktree" });
    await waitFor(() => expect(confirmButton).not.toBeDisabled());
    fireEvent.click(confirmButton);
    expect(onConfirm).toHaveBeenCalledWith({
      expect: { head: "cafefeed", dirty: 1, untracked: 2, unpushed: 0 },
    });
  });

  it("confirming an unpushed-commits removal with Force ticked sends force:true plus the same expect block", async () => {
    preflightMock.mockResolvedValue(
      worktreePreflight({
        action: "remove",
        head: "abc0000",
        dirty: 0,
        untracked: 0,
        unpushed: 2,
        blocked: ["unpushed-commits"],
      })
    );
    const onConfirm = vi.fn();
    render(
      <DestructiveLaneModal
        lane={makeLane()}
        action="remove"
        open
        onClose={vi.fn()}
        onConfirm={onConfirm}
      />
    );
    const confirmButton = await screen.findByRole("button", { name: "Remove lane" });
    fireEvent.click(screen.getByRole("checkbox"));
    await waitFor(() => expect(confirmButton).not.toBeDisabled());
    fireEvent.click(confirmButton);
    expect(onConfirm).toHaveBeenCalledWith({
      expect: { head: "abc0000", dirty: 0, untracked: 0, unpushed: 2 },
      force: true,
    });
  });

  it("confirming a purge passes back the purge-specific expect block", async () => {
    const purgePreflight: LanePurgePreflight = {
      action: "purge",
      lane: 1,
      sessions: 4,
      events: 120,
      tokenRows: 30,
      bytesEstimate: 4096,
      activeSessionSkipped: false,
    };
    preflightMock.mockResolvedValue(purgePreflight);
    const onConfirm = vi.fn();
    render(
      <DestructiveLaneModal
        lane={makeLane()}
        action="purge"
        open
        onClose={vi.fn()}
        onConfirm={onConfirm}
      />
    );
    const confirmButton = await screen.findByRole("button", { name: "Purge history" });
    await waitFor(() => expect(confirmButton).not.toBeDisabled());
    fireEvent.click(confirmButton);
    expect(onConfirm).toHaveBeenCalledWith({
      expect: { sessions: 4, events: 120, tokenRows: 30 },
    });
  });

  it("shows the purge size estimate and says when a live session was spared", async () => {
    const purgePreflight: LanePurgePreflight = {
      action: "purge",
      lane: 1,
      sessions: 2,
      events: 8,
      tokenRows: 2,
      bytesEstimate: 5120,
      activeSessionSkipped: true,
    };
    preflightMock.mockResolvedValue(purgePreflight);
    render(
      <DestructiveLaneModal
        lane={makeLane()}
        action="purge"
        open
        onClose={vi.fn()}
        onConfirm={vi.fn()}
      />
    );
    expect(await screen.findByText("5.0 KB")).toBeInTheDocument();
    expect(screen.getByText(/still active and will be kept/)).toBeInTheDocument();
  });

  it("does not claim a session was spared when none was", async () => {
    const purgePreflight: LanePurgePreflight = {
      action: "purge",
      lane: 1,
      sessions: 1,
      events: 1,
      tokenRows: 0,
      bytesEstimate: 512,
      activeSessionSkipped: false,
    };
    preflightMock.mockResolvedValue(purgePreflight);
    render(
      <DestructiveLaneModal
        lane={makeLane()}
        action="purge"
        open
        onClose={vi.fn()}
        onConfirm={vi.fn()}
      />
    );
    expect(await screen.findByText("512 B")).toBeInTheDocument();
    expect(screen.queryByText(/still active and will be kept/)).not.toBeInTheDocument();
  });
});
