/**
 * L8 — git feature wired to the stateful `MockGitGateway` via the real
 * `DIProvider`.
 *
 * Covers:
 * - status shows staged/unstaged groupings
 * - stage moves a file from Unstaged to Staged
 * - commit with empty message is blocked (button disabled)
 * - commit with message but nothing staged is blocked (button disabled)
 * - commit moves staged files out and adds an entry to the log
 * - checkout changes the current branch
 * - MockGitGateway unit behaviour
 */

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

import { MockGitGateway } from "@/adapters/mock";
import type { Gateways } from "@/ports";
import { DIProvider } from "@/app/di";
import { GitPanel } from "./GitPanel";

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

const PROJECT_ID = "proj-git-test-001";

function renderPanel(git: MockGitGateway = new MockGitGateway()) {
  const gateways = { git } as unknown as Gateways;
  return {
    git,
    ...render(
      <DIProvider gateways={gateways}>
        <GitPanel projectId={PROJECT_ID} />
      </DIProvider>,
    ),
  };
}

/** Waits until the "Changes" section is rendered (panel stabilised). */
async function waitForPanel() {
  await waitFor(() => {
    expect(screen.getByRole("region", { name: "Changes" })).toBeTruthy();
  });
}

// ---------------------------------------------------------------------------
// Integration tests (GitPanel + MockGitGateway via DIProvider)
// ---------------------------------------------------------------------------

describe("GitPanel (with MockGitGateway)", () => {
  it("shows demo files split between Staged and Unstaged sections", async () => {
    const git = new MockGitGateway();
    // Seed a staged file.
    await git.init(PROJECT_ID);
    await git.stage(PROJECT_ID, "src/main.rs");

    renderPanel(git);
    await waitForPanel();

    expect(screen.getByText("Staged")).toBeTruthy();
    expect(screen.getByText("Unstaged")).toBeTruthy();
    // src/main.rs is staged → Unstage button exists
    expect(
      screen.getByRole("button", { name: "unstage src/main.rs" }),
    ).toBeTruthy();
    // README.md is unstaged → Stage button exists
    expect(
      screen.getByRole("button", { name: "stage README.md" }),
    ).toBeTruthy();
  });

  it("clicking Stage moves a file to the Staged group", async () => {
    renderPanel();
    await waitForPanel();

    // Stage README.md
    const stageBtn = await screen.findByRole("button", {
      name: "stage README.md",
    });
    fireEvent.click(stageBtn);

    // After staging, an Unstage button should appear for README.md
    await waitFor(() => {
      expect(
        screen.getByRole("button", { name: "unstage README.md" }),
      ).toBeTruthy();
    });
  });

  it("clicking Unstage moves a file back to Unstaged", async () => {
    const git = new MockGitGateway();
    await git.init(PROJECT_ID);
    await git.stage(PROJECT_ID, "src/main.rs");

    renderPanel(git);
    await waitForPanel();

    const unstageBtn = await screen.findByRole("button", {
      name: "unstage src/main.rs",
    });
    fireEvent.click(unstageBtn);

    await waitFor(() => {
      expect(
        screen.getByRole("button", { name: "stage src/main.rs" }),
      ).toBeTruthy();
    });
  });

  it("Commit button is disabled when message is empty", async () => {
    renderPanel();
    await waitForPanel();

    const btn = screen.getByRole("button", { name: /commit/i });
    expect((btn as HTMLButtonElement).disabled).toBe(true);
  });

  it("Commit button is disabled when message is set but nothing is staged", async () => {
    renderPanel();
    await waitForPanel();

    fireEvent.change(screen.getByLabelText("commit message"), {
      target: { value: "my message" },
    });

    const btn = screen.getByRole("button", { name: /commit/i });
    expect((btn as HTMLButtonElement).disabled).toBe(true);
  });

  it("successful commit removes staged files and adds to the log", async () => {
    const git = new MockGitGateway();
    await git.init(PROJECT_ID);
    await git.stage(PROJECT_ID, "src/main.rs");
    await git.stage(PROJECT_ID, "README.md");

    renderPanel(git);
    await waitForPanel();

    fireEvent.change(screen.getByLabelText("commit message"), {
      target: { value: "initial commit" },
    });

    const btn = screen.getByRole("button", { name: /commit/i });
    expect((btn as HTMLButtonElement).disabled).toBe(false);
    fireEvent.click(btn);

    // After commit, no files should remain (both were staged).
    await waitFor(() => {
      expect(screen.getByText("No changes.")).toBeTruthy();
    });

    // The commit should appear in the log.
    await waitFor(() => {
      expect(screen.getByText("initial commit")).toBeTruthy();
    });
  });

  it("checkout changes the current branch label", async () => {
    renderPanel();
    await waitForPanel();

    // Ensure the Branches section is rendered with "main" as current.
    const branchesSection = await screen.findByRole("region", {
      name: "Branches",
    });
    expect(branchesSection).toBeTruthy();

    // Create & checkout a new branch via the form.
    const branchInput = screen.getByLabelText("new branch name");
    fireEvent.change(branchInput, { target: { value: "feature/new-thing" } });
    fireEvent.click(screen.getByRole("button", { name: /create.*checkout/i }));

    // After checkout, the new branch should appear (may show in current + list).
    await waitFor(() => {
      const matches = screen.getAllByText("feature/new-thing");
      expect(matches.length).toBeGreaterThan(0);
    });

    // A checkout button for "main" should now appear (since it's no longer current).
    await waitFor(() => {
      expect(
        screen.getByRole("button", { name: "checkout main" }),
      ).toBeTruthy();
    });
  });

  it("shows 'No commits yet.' in the Log section initially", async () => {
    renderPanel();
    await waitForPanel();

    expect(screen.getByText("No commits yet.")).toBeTruthy();
  });
});

// ---------------------------------------------------------------------------
// MockGitGateway unit tests
// ---------------------------------------------------------------------------

describe("MockGitGateway (unit)", () => {
  it("status returns seeded files for a new project", async () => {
    const gw = new MockGitGateway();
    const files = await gw.status("proj");
    expect(files.length).toBeGreaterThan(0);
    expect(files.every((f) => !f.staged)).toBe(true);
  });

  it("init creates state if absent", async () => {
    const gw = new MockGitGateway();
    await gw.init("new-proj");
    const files = await gw.status("new-proj");
    expect(Array.isArray(files)).toBe(true);
  });

  it("stage toggles a file to staged=true", async () => {
    const gw = new MockGitGateway();
    const before = await gw.status("p");
    const target = before[0].path;
    await gw.stage("p", target);
    const after = await gw.status("p");
    const file = after.find((f) => f.path === target)!;
    expect(file.staged).toBe(true);
  });

  it("unstage toggles a file back to staged=false", async () => {
    const gw = new MockGitGateway();
    const files = await gw.status("p");
    const target = files[0].path;
    await gw.stage("p", target);
    await gw.unstage("p", target);
    const after = await gw.status("p");
    expect(after.find((f) => f.path === target)!.staged).toBe(false);
  });

  it("commit removes staged files from status and adds to log", async () => {
    const gw = new MockGitGateway();
    const files = await gw.status("p");
    const target = files[0].path;
    await gw.stage("p", target);
    const result = await gw.commit("p", "test commit\n\nmore text");
    expect(result.summary).toBe("test commit");
    expect(result.hash).toMatch(/^mock-/);
    const after = await gw.status("p");
    expect(after.find((f) => f.path === target)).toBeUndefined();
    const log = await gw.log("p", 10);
    expect(log[0].summary).toBe("test commit");
  });

  it("branches returns initial ['main'] with current='main'", async () => {
    const gw = new MockGitGateway();
    const b = await gw.branches("p");
    expect(b.branches).toEqual(["main"]);
    expect(b.current).toBe("main");
  });

  it("checkout creates a new branch and sets it as current", async () => {
    const gw = new MockGitGateway();
    await gw.checkout("p", "develop");
    const b = await gw.branches("p");
    expect(b.branches).toContain("develop");
    expect(b.current).toBe("develop");
  });

  it("log(limit) returns at most limit entries", async () => {
    const gw = new MockGitGateway();
    const files = await gw.status("p");
    // Stage and commit each file individually.
    for (let i = 0; i < files.length; i++) {
      await gw.stage("p", files[i].path);
      await gw.commit("p", `commit ${i}`);
    }
    const log = await gw.log("p", 1);
    expect(log).toHaveLength(1);
  });

  it("projects are isolated", async () => {
    const gw = new MockGitGateway();
    await gw.stage("proj-A", (await gw.status("proj-A"))[0].path);
    await gw.commit("proj-A", "A commit");
    const logB = await gw.log("proj-B", 10);
    expect(logB).toHaveLength(0);
  });
});
