// @vitest-environment happy-dom
import "@testing-library/jest-dom/vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { vi, describe, it, expect, beforeEach } from "vitest";
import { FileCard } from "./FileCard";
import type { FileDiff, FileDiffDetail } from "./types";

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

vi.mock("./HunkRows", () => ({
  HunkRows: ({ hunk }: { hunk: { header: string } }) => (
    <tr data-testid="hunk-row">
      <td>{hunk.header}</td>
    </tr>
  ),
}));

vi.mock("./use-syntax-highlight", () => ({
  useHighlightedFile: () => null,
}));

// Hoist mocks so they can be referenced in vi.mock factories
const { mockGetFileDiff, mockParseUnifiedDiff } = vi.hoisted(() => ({
  mockGetFileDiff: vi.fn(),
  mockParseUnifiedDiff: vi.fn(),
}));

vi.mock("@/lib/trpc", () => ({
  trpc: {
    getFileDiff: {
      useQuery: (
        input: unknown,
        opts: { enabled: boolean; staleTime?: number },
      ) => mockGetFileDiff(input, opts),
    },
  },
}));

vi.mock("./parse-diff", () => ({
  parseUnifiedDiff: (rawDiff: string) => mockParseUnifiedDiff(rawDiff),
}));

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

function makeFile(overrides: Partial<FileDiff> = {}): FileDiff {
  return {
    oldPath: "src/foo.ts",
    newPath: "src/foo.ts",
    status: "modified",
    additions: 10,
    deletions: 5,
    ...overrides,
  };
}

const defaultProps = {
  phaseId: "phase-1",
  commitMode: false,
  commentsByLine: new Map(),
  onAddComment: vi.fn(),
  onResolveComment: vi.fn(),
  onUnresolveComment: vi.fn(),
};

beforeEach(() => {
  mockGetFileDiff.mockReturnValue({
    data: undefined,
    isLoading: false,
    isError: false,
    refetch: vi.fn(),
  });
  // Default: return empty parse result
  mockParseUnifiedDiff.mockReturnValue([]);
});

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

describe("FileCard", () => {
  it("starts collapsed and does not enable getFileDiff query", () => {
    render(<FileCard file={makeFile()} {...defaultProps} />);

    // Query must be called with enabled: false while card is collapsed
    expect(mockGetFileDiff).toHaveBeenCalledWith(
      expect.objectContaining({
        filePath: encodeURIComponent("src/foo.ts"),
      }),
      expect.objectContaining({ enabled: false }),
    );

    // No hunk rows rendered in the collapsed state
    expect(screen.queryByTestId("hunk-row")).toBeNull();
  });

  it("enables query and shows loading spinner when expanded", () => {
    mockGetFileDiff.mockReturnValue({
      data: undefined,
      isLoading: true,
      isError: false,
      refetch: vi.fn(),
    });

    render(<FileCard file={makeFile()} {...defaultProps} />);
    fireEvent.click(screen.getByRole("button"));

    // After expanding, query should be called with enabled: true
    expect(mockGetFileDiff).toHaveBeenLastCalledWith(
      expect.anything(),
      expect.objectContaining({ enabled: true }),
    );

    // Loading spinner should be visible
    expect(screen.getByText(/Loading diff/i)).toBeInTheDocument();
  });

  it("renders HunkRows when query succeeds", async () => {
    mockGetFileDiff.mockReturnValue({
      data: {
        binary: false,
        rawDiff:
          "diff --git a/src/foo.ts b/src/foo.ts\n@@ -1,3 +1,3 @@\n context\n",
      },
      isLoading: false,
      isError: false,
      refetch: vi.fn(),
    });

    mockParseUnifiedDiff.mockReturnValue([
      {
        oldPath: "src/foo.ts",
        newPath: "src/foo.ts",
        status: "modified",
        additions: 0,
        deletions: 0,
        hunks: [
          {
            header: "@@ -1,3 +1,3 @@",
            oldStart: 1,
            oldCount: 3,
            newStart: 1,
            newCount: 3,
            lines: [],
          },
        ],
      },
    ]);

    render(<FileCard file={makeFile()} {...defaultProps} />);
    fireEvent.click(screen.getByRole("button"));

    await waitFor(() => {
      expect(screen.getByTestId("hunk-row")).toBeInTheDocument();
    });
  });

  it("shows error state with Retry button; clicking retry calls refetch", () => {
    const refetch = vi.fn();
    mockGetFileDiff.mockReturnValue({
      data: undefined,
      isLoading: false,
      isError: true,
      refetch,
    });

    render(<FileCard file={makeFile()} {...defaultProps} />);
    fireEvent.click(screen.getByRole("button"));

    expect(screen.getByText(/Failed to load diff/i)).toBeInTheDocument();
    const retryBtn = screen.getByRole("button", { name: /retry/i });
    fireEvent.click(retryBtn);
    expect(refetch).toHaveBeenCalledOnce();
  });

  it("shows binary message on expand and does not enable getFileDiff query", () => {
    render(<FileCard file={makeFile({ status: "binary" })} {...defaultProps} />);
    fireEvent.click(screen.getByRole("button"));

    expect(screen.getByText(/Binary file/i)).toBeInTheDocument();

    // Query must never be enabled for binary files
    expect(mockGetFileDiff).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({ enabled: false }),
    );
  });

  it("shows No content changes when parsed hunks array is empty", async () => {
    mockGetFileDiff.mockReturnValue({
      data: {
        binary: false,
        rawDiff: "diff --git a/src/foo.ts b/src/foo.ts\nsome content\n",
      },
      isLoading: false,
      isError: false,
      refetch: vi.fn(),
    });

    mockParseUnifiedDiff.mockReturnValue([
      {
        oldPath: "src/foo.ts",
        newPath: "src/foo.ts",
        status: "modified",
        additions: 0,
        deletions: 0,
        hunks: [],
      },
    ]);

    render(<FileCard file={makeFile()} {...defaultProps} />);
    fireEvent.click(screen.getByRole("button"));

    await waitFor(() => {
      expect(screen.getByText(/No content changes/i)).toBeInTheDocument();
    });
  });

  it("renders pre-parsed hunks from detail prop without fetching", () => {
    const detail: FileDiffDetail = {
      oldPath: "src/foo.ts",
      newPath: "src/foo.ts",
      status: "modified",
      additions: 5,
      deletions: 2,
      hunks: [
        {
          header: "@@ -1 +1 @@",
          oldStart: 1,
          oldCount: 1,
          newStart: 1,
          newCount: 1,
          lines: [],
        },
      ],
    };

    render(<FileCard file={makeFile()} detail={detail} {...defaultProps} />);

    // Should start expanded because detail prop is provided
    expect(screen.getByTestId("hunk-row")).toBeInTheDocument();

    // Query must not be enabled when detail prop is present
    expect(mockGetFileDiff).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({ enabled: false }),
    );
  });

  it("does not refetch when collapsing and re-expanding", () => {
    // Simulate data already available (as if previously fetched and cached)
    mockGetFileDiff.mockReturnValue({
      data: { binary: false, rawDiff: "" },
      isLoading: false,
      isError: false,
      refetch: vi.fn(),
    });

    render(<FileCard file={makeFile()} {...defaultProps} />);
    const headerBtn = screen.getByRole("button");

    // Expand: query enabled, data shown immediately (no loading)
    fireEvent.click(headerBtn);
    expect(screen.queryByText(/Loading diff/i)).toBeNull();

    // Collapse
    fireEvent.click(headerBtn);

    // Re-expand: should not enter loading state (data still available)
    fireEvent.click(headerBtn);
    expect(screen.queryByText(/Loading diff/i)).toBeNull();
  });
});
