import { describe, expect, test } from "bun:test";
import type { BranchMeta, LifecycleState } from "@decocms/sandbox/shared";
import type { ClaimPhase } from "@/components/sandbox/hooks/sandbox-events-context";
import { selectHeaderButton } from "./panel-state";
import type { CheckRun, PrSummary } from "./use-pr-data";
import type { PrReviewSignals } from "./use-pr-reviews";
import type { TFunction, TranslationKey } from "@/i18n/use-t.ts";
import type { InterpolationVars } from "@/i18n/interpolate.ts";
import { thread as threadEn } from "@/i18n/en/thread.ts";

const mockT: TFunction = (key: TranslationKey, vars?: InterpolationVars) => {
  const template =
    (threadEn as Record<string, string>)[key as string] ?? (key as string);
  if (!vars) return template;
  return Object.entries(vars).reduce(
    (s, [k, v]) => s.replace(`{${k}}`, String(v)),
    template,
  );
};

type ReadyBranch = Extract<BranchMeta, { kind: "ready" }>;

const RUNNING_LIFECYCLE: LifecycleState = {
  phase: "running",
  port: 3000,
  htmlSupport: true,
};

function ready(over: Partial<ReadyBranch> = {}): ReadyBranch {
  return {
    kind: "ready",
    branch: "feat/x",
    base: "main",
    workingTreeDirty: false,
    unpushed: 0,
    aheadOfBase: 0,
    behindBase: 0,
    headSha: "abc123",
    ...over,
  };
}

interface BaseInput {
  lifecycle: LifecycleState;
  branch: BranchMeta;
  claimPhase: ClaimPhase | null;
  pr: PrSummary | null;
  checks: CheckRun[];
  reviews: PrReviewSignals | null;
  loading?: boolean;
  t: TFunction;
}

function happyInput(over: Partial<BaseInput> = {}): BaseInput {
  return {
    lifecycle: RUNNING_LIFECYCLE,
    branch: ready(),
    claimPhase: null,
    pr: null,
    checks: [],
    reviews: null,
    t: mockT,
    ...over,
  };
}

function pr(over: Partial<PrSummary> = {}): PrSummary {
  return {
    number: 42,
    title: "Add X",
    body: "",
    state: "open",
    merged: false,
    mergedAt: null,
    base: "main",
    head: "feat/x",
    headSha: "abc123",
    headRepoFullName: "acme/web",
    htmlUrl: "https://github.com/acme/web/pull/42",
    author: "me",
    ...over,
  };
}

function check(over: Partial<CheckRun> = {}): CheckRun {
  return {
    id: "1",
    name: "lint",
    status: "completed",
    conclusion: "success",
    htmlUrl: "",
    durationMs: null,
    ...over,
  };
}

function reviews(over: Partial<PrReviewSignals> = {}): PrReviewSignals {
  return {
    draft: false,
    mergeableState: "clean",
    unresolvedConversations: 0,
    missingRequiredApprovals: false,
    ...over,
  };
}

describe("selectHeaderButton", () => {
  test("loading flag → Loading… (disabled, spinner)", () => {
    const r = selectHeaderButton(happyInput({ loading: true }));
    expect(r.label).toBe("Loading…");
    expect(r.disabled).toBe(true);
    expect(r.loading).toBe(true);
    expect(r.variant).toBe("outline");
  });

  test("lifecycle.idle → 'Preparing environment…' (disabled, spinner)", () => {
    const r = selectHeaderButton(
      happyInput({
        lifecycle: { phase: "idle" },
        branch: { kind: "unknown" },
        claimPhase: null,
      }),
    );
    expect(r.label).toBe("Preparing environment…");
    expect(r.disabled).toBe(true);
    expect(r.loading).toBe(true);
    expect(r.variant).toBe("outline");
  });

  test("idle + claimPhase = waiting-for-capacity → 'Waiting for capacity'", () => {
    const r = selectHeaderButton(
      happyInput({
        lifecycle: { phase: "idle" },
        branch: { kind: "unknown" },
        claimPhase: { kind: "waiting-for-capacity", since: 0 },
      }),
    );
    expect(r.label).toBe("Waiting for capacity");
  });

  test("idle + claimPhase = pulling-image → 'Downloading image'", () => {
    const r = selectHeaderButton(
      happyInput({
        lifecycle: { phase: "idle" },
        branch: { kind: "unknown" },
        claimPhase: { kind: "pulling-image", since: 0 },
      }),
    );
    expect(r.label).toBe("Downloading image");
  });

  test("idle + claimPhase = starting-container → 'Starting container'", () => {
    const r = selectHeaderButton(
      happyInput({
        lifecycle: { phase: "idle" },
        branch: { kind: "unknown" },
        claimPhase: { kind: "starting-container", since: 0 },
      }),
    );
    expect(r.label).toBe("Starting container");
  });

  test("idle + claimPhase = warming-daemon → 'Connecting to sandbox'", () => {
    const r = selectHeaderButton(
      happyInput({
        lifecycle: { phase: "idle" },
        branch: { kind: "unknown" },
        claimPhase: { kind: "warming-daemon", since: 0 },
      }),
    );
    expect(r.label).toBe("Connecting to sandbox");
  });

  test("idle + claimPhase = ready → 'Preparing environment…' (generic)", () => {
    // ready means the daemon claim handle is up but lifecycle hasn't yet
    // emitted its first event. Generic copy is correct here.
    const r = selectHeaderButton(
      happyInput({
        lifecycle: { phase: "idle" },
        branch: { kind: "unknown" },
        claimPhase: { kind: "ready" },
      }),
    );
    expect(r.label).toBe("Preparing environment…");
  });

  test("idle + claimPhase = claiming → 'Preparing environment…' (generic)", () => {
    // `claiming` is intentionally absent from `idleClaimCopy` — falls through
    // to the generic label.
    const r = selectHeaderButton(
      happyInput({
        lifecycle: { phase: "idle" },
        branch: { kind: "unknown" },
        claimPhase: { kind: "claiming", since: 0 },
      }),
    );
    expect(r.label).toBe("Preparing environment…");
  });

  test("lifecycle.cloning → 'Cloning repo…' (disabled, spinner)", () => {
    const r = selectHeaderButton(
      happyInput({
        lifecycle: { phase: "cloning" },
        branch: { kind: "unknown" },
      }),
    );
    expect(r.label).toBe("Cloning repo…");
    expect(r.loading).toBe(true);
  });

  test("lifecycle.clone-failed → 'Clone failed' with error tooltip", () => {
    const r = selectHeaderButton(
      happyInput({
        lifecycle: { phase: "clone-failed", error: "exit 128" },
        branch: { kind: "unknown" },
      }),
    );
    expect(r.label).toBe("Clone failed");
    expect(r.disabled).toBe(true);
    expect(r.tooltip).toBe("exit 128");
  });

  test("lifecycle.checking-out → 'Switching to <branch>…'", () => {
    const r = selectHeaderButton(
      happyInput({
        lifecycle: { phase: "checking-out", to: "feat/y" },
        branch: { kind: "unknown" },
      }),
    );
    expect(r.label).toBe("Switching to feat/y…");
    expect(r.loading).toBe(true);
  });

  // Post-clone lifecycle phases (installing / starting / install-failed /
  // start-failed / crashed) intentionally fall through to git/PR logic.
  // Git operates on the cloned repo independently of dev-server health;
  // the user can commit fixes or push a hotfix even when the dev server
  // can't run.

  test("lifecycle.installing + dirty branch → Submit for review (fall-through)", () => {
    const r = selectHeaderButton(
      happyInput({
        lifecycle: { phase: "installing" },
        branch: ready({ workingTreeDirty: true }),
      }),
    );
    expect(r.label).toBe("Submit for review");
  });

  test("lifecycle.starting + clean ready branch → Up to date (fall-through)", () => {
    const r = selectHeaderButton(
      happyInput({ lifecycle: { phase: "starting" } }),
    );
    expect(r.label).toBe("Up to date");
  });

  test("lifecycle.install-failed + dirty branch → Submit for review (commit fixes)", () => {
    const r = selectHeaderButton(
      happyInput({
        lifecycle: { phase: "install-failed", error: "ENOENT package.json" },
        branch: ready({ workingTreeDirty: true }),
      }),
    );
    expect(r.label).toBe("Submit for review");
  });

  test("lifecycle.start-failed + ahead-of-base → Submit for review", () => {
    const r = selectHeaderButton(
      happyInput({
        lifecycle: { phase: "start-failed", error: "exit 1" },
        branch: ready({ aheadOfBase: 3 }),
      }),
    );
    expect(r.label).toBe("Submit for review");
  });

  test("lifecycle.crashed + dirty branch → Submit for review (push hotfix)", () => {
    const r = selectHeaderButton(
      happyInput({
        lifecycle: { phase: "crashed" },
        branch: ready({ workingTreeDirty: true }),
      }),
    );
    expect(r.label).toBe("Submit for review");
  });

  test("post-clone with branch still unknown → Loading branch… (defensive)", () => {
    // Brief window between checkout completing and the daemon emitting
    // the `branch` event with git metadata.
    const r = selectHeaderButton(
      happyInput({
        lifecycle: { phase: "installing" },
        branch: { kind: "unknown" },
      }),
    );
    expect(r.label).toBe("Loading branch…");
    expect(r.loading).toBe(true);
  });

  test("no diff anywhere → Up to date (disabled, outline)", () => {
    const r = selectHeaderButton(happyInput());
    expect(r.label).toBe("Up to date");
    expect(r.disabled).toBe(true);
    expect(r.variant).toBe("outline");
  });

  test("dirty working tree → Submit for review + side Publish", () => {
    const r = selectHeaderButton(
      happyInput({ branch: ready({ workingTreeDirty: true }) }),
    );
    expect(r.label).toBe("Submit for review");
    expect(r.action).toBe("create-pr");
    expect(r.disabled).toBeFalsy();
    expect(r.showPublishSide).toBe(true);
  });

  test("unpushed commits ahead of base with no PR → Submit for review", () => {
    const r = selectHeaderButton(
      happyInput({ branch: ready({ unpushed: 2, aheadOfBase: 2 }) }),
    );
    expect(r.label).toBe("Submit for review");
    expect(r.action).toBe("create-pr");
    expect(r.showPublishSide).toBe(true);
  });

  test("unpushed commits without base divergence and no PR → Submit for review", () => {
    const r = selectHeaderButton(
      happyInput({ branch: ready({ unpushed: 2, aheadOfBase: 0 }) }),
    );
    expect(r.label).toBe("Submit for review");
    expect(r.action).toBe("create-pr");
  });

  test("unpushed commits with open PR (different head) → Submit for review", () => {
    const r = selectHeaderButton(
      happyInput({
        branch: ready({ unpushed: 2, aheadOfBase: 2, headSha: "local999" }),
        pr: pr({ headSha: "remote888" }),
      }),
    );
    expect(r.label).toBe("Submit for review");
    expect(r.action).toBe("create-pr");
  });

  test("false-positive unpushed (headSha matches PR) → falls through to merge", () => {
    const r = selectHeaderButton(
      happyInput({
        branch: ready({ unpushed: 1, aheadOfBase: 1, headSha: "abc123" }),
        pr: pr({ headSha: "abc123" }),
        reviews: reviews(),
      }),
    );
    expect(r.label).toBe("Publish to production");
    expect(r.action).toBe("merge-split");
    expect(r.variant).toBe("success");
    expect(r.tooltip).toBe("Squash-merge PR #42 into main");
  });

  test("ahead of base + closed non-merged PR → Reopen PR", () => {
    const r = selectHeaderButton(
      happyInput({
        branch: ready({ aheadOfBase: 3 }),
        pr: pr({ state: "closed", merged: false }),
      }),
    );
    expect(r.label).toBe("Reopen");
    expect(r.action).toBe("reopen");
  });

  test("merged PR, branch at merge head → Published (disabled, outline)", () => {
    const r = selectHeaderButton(
      happyInput({
        branch: ready({ aheadOfBase: 3, headSha: "abc123" }),
        pr: pr({
          state: "closed",
          merged: true,
          mergedAt: "2026-04-22T00:00:00Z",
          headSha: "abc123",
        }),
      }),
    );
    expect(r.label).toBe("Published");
    expect(r.disabled).toBe(true);
    expect(r.variant).toBe("outline");
  });

  test("merged PR, branch advanced past merge head → Continue (special)", () => {
    const r = selectHeaderButton(
      happyInput({
        branch: ready({ aheadOfBase: 4, headSha: "def456" }),
        pr: pr({
          state: "closed",
          merged: true,
          mergedAt: "2026-04-22T00:00:00Z",
          headSha: "abc123",
        }),
      }),
    );
    expect(r.label).toBe("Continue");
    expect(r.action).toBe("create-pr");
    expect(r.variant).toBe("special");
  });

  test("ahead of base + no PR → Submit for review", () => {
    const r = selectHeaderButton(
      happyInput({ branch: ready({ aheadOfBase: 3 }) }),
    );
    expect(r.label).toBe("Submit for review");
  });

  test("PR open + mergeable_state=dirty → Sync with main", () => {
    const r = selectHeaderButton(
      happyInput({
        branch: ready({ aheadOfBase: 3 }),
        pr: pr(),
        reviews: reviews({ mergeableState: "dirty" }),
      }),
    );
    expect(r.label).toBe("Sync with main");
    expect(r.action).toBe("rebase");
  });

  test("PR open + failed check → Fix tests with failing list", () => {
    const r = selectHeaderButton(
      happyInput({
        branch: ready({ aheadOfBase: 3 }),
        pr: pr(),
        checks: [check({ conclusion: "failure", name: "unit-test" })],
        reviews: reviews(),
      }),
    );
    expect(r.label).toBe("Fix tests");
    expect(r.action).toBe("fix-checks");
    expect(r.meta?.failingChecks).toEqual(["unit-test"]);
  });

  test("PR open + check in-progress → Running tests…", () => {
    const r = selectHeaderButton(
      happyInput({
        branch: ready({ aheadOfBase: 3 }),
        pr: pr(),
        checks: [check({ status: "in_progress", conclusion: null })],
        reviews: reviews(),
      }),
    );
    expect(r.label).toBe("Running tests…");
    expect(r.disabled).toBe(true);
    expect(r.loading).toBe(true);
    expect(r.variant).toBe("outline");
  });

  test("PR open + draft → Mark ready", () => {
    const r = selectHeaderButton(
      happyInput({
        branch: ready({ aheadOfBase: 3 }),
        pr: pr(),
        reviews: reviews({ draft: true }),
      }),
    );
    expect(r.label).toBe("Mark ready");
    expect(r.action).toBe("mark-ready");
  });

  test("PR open + unresolved conversations → Address feedback", () => {
    const r = selectHeaderButton(
      happyInput({
        branch: ready({ aheadOfBase: 3 }),
        pr: pr(),
        reviews: reviews({ unresolvedConversations: 2 }),
      }),
    );
    expect(r.label).toBe("Address feedback");
    expect(r.action).toBe("resolve-comments");
  });

  test("PR open + missing approvals → Awaiting review", () => {
    const r = selectHeaderButton(
      happyInput({
        branch: ready({ aheadOfBase: 3 }),
        pr: pr(),
        reviews: reviews({ missingRequiredApprovals: true }),
      }),
    );
    expect(r.label).toBe("Awaiting review");
    expect(r.disabled).toBe(true);
  });

  test("PR open + all clear → Publish", () => {
    const r = selectHeaderButton(
      happyInput({
        branch: ready({ aheadOfBase: 3 }),
        pr: pr(),
        checks: [check()],
        reviews: reviews(),
      }),
    );
    expect(r.label).toBe("Publish to production");
    expect(r.action).toBe("merge-split");
    expect(r.variant).toBe("success");
    // merge-split IS the publish action — no redundant side Publish button.
    expect(r.showPublishSide).toBeFalsy();
  });

  test("PR open + reviews still loading → Publish to production (main base)", () => {
    const r = selectHeaderButton(
      happyInput({
        branch: ready({ aheadOfBase: 3 }),
        pr: pr(),
        checks: [check()],
        reviews: null,
      }),
    );
    expect(r.label).toBe("Publish to production");
    expect(r.action).toBe("merge-split");
  });

  test("PR open + all clear + base develop → Publish", () => {
    const r = selectHeaderButton(
      happyInput({
        branch: ready({ aheadOfBase: 3, base: "develop" }),
        pr: pr({ base: "develop" }),
        checks: [check()],
        reviews: reviews(),
      }),
    );
    expect(r.label).toBe("Publish");
    expect(r.action).toBe("merge-split");
  });

  test("priority: dirty beats everything else", () => {
    const r = selectHeaderButton(
      happyInput({
        branch: ready({ workingTreeDirty: true, aheadOfBase: 3 }),
        pr: pr(),
        checks: [check({ conclusion: "failure" })],
        reviews: reviews({ mergeableState: "dirty" }),
      }),
    );
    expect(r.label).toBe("Submit for review");
  });

  test("priority inside PR open: conflicts beat failed checks", () => {
    const r = selectHeaderButton(
      happyInput({
        branch: ready({ aheadOfBase: 3 }),
        pr: pr(),
        checks: [check({ conclusion: "failure" })],
        reviews: reviews({ mergeableState: "dirty" }),
      }),
    );
    expect(r.label).toBe("Sync with main");
  });

  test("priority: failed checks beat in-progress checks", () => {
    const r = selectHeaderButton(
      happyInput({
        branch: ready({ aheadOfBase: 3 }),
        pr: pr(),
        checks: [
          check({ conclusion: "failure", name: "lint" }),
          check({ status: "in_progress", conclusion: null, name: "unit-test" }),
        ],
        reviews: reviews(),
      }),
    );
    expect(r.label).toBe("Fix tests");
  });
});
