/**
 * @vitest-environment jsdom
 */
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { setApiInstance } from "@multica/core/api";
import type { ApiClient } from "@multica/core/api/client";
import { pruneIssueSurfaceViewStates } from "@multica/core/issues/stores/surface-view-store";
import type {
  AgentTask,
  Issue,
  ListIssuesParams,
  ListIssuesResponse,
} from "@multica/core/types";
import { IssueSurface } from "./issue-surface";

// Mutable so tests can simulate a workspace switch — the workspace layout
// does not remount its children on switch, so the surface must handle the
// wsId change itself.
const mockWsId = vi.hoisted(() => ({ current: "ws-1" }));
vi.mock("@multica/core/hooks", () => ({
  useWorkspaceId: () => mockWsId.current,
}));

const mockAuthUser = { id: "user-1", email: "test@test.com", name: "Test User" };
vi.mock("@multica/core/auth", () => ({
  useAuthStore: Object.assign(
    (selector?: (state: unknown) => unknown) => {
      const state = { user: mockAuthUser, isAuthenticated: true };
      return selector ? selector(state) : state;
    },
    { getState: () => ({ user: mockAuthUser, isAuthenticated: true }) },
  ),
  registerAuthStore: vi.fn(),
  createAuthStore: vi.fn(),
}));

vi.mock("../../i18n", () => ({
  useT: () => ({ t: () => "translated" }),
  useTimeAgo: () => () => "now",
}));

vi.mock("../../navigation", () => ({
  AppLink: ({ children, href, ...props }: React.ComponentProps<"a">) => (
    <a href={href} {...props}>
      {children}
    </a>
  ),
  useNavigation: () => ({ push: vi.fn(), pathname: "/" }),
}));

vi.mock("@multica/core/paths", async () => {
  const actual = await vi.importActual<typeof import("@multica/core/paths")>(
    "@multica/core/paths",
  );
  return {
    ...actual,
    useCurrentWorkspace: () => ({ id: "ws-1", name: "Test WS", slug: "test" }),
    useWorkspacePaths: () => actual.paths.workspace("test"),
  };
});

function makeIssue(id: string, title: string, projectId: string): Issue {
  return {
    id,
    workspace_id: "ws-1",
    number: 1,
    identifier: `MUL-${id}`,
    title,
    description: null,
    status: "todo",
    priority: "none",
    assignee_type: null,
    assignee_id: null,
    creator_type: "member",
    creator_id: "user-1",
    parent_issue_id: null,
    project_id: projectId,
    position: 1,
    stage: null,
    start_date: null,
    due_date: null,
    labels: [],
    metadata: {},
    created_at: "2026-01-01T00:00:00Z",
    updated_at: "2026-01-01T00:00:00Z",
  };
}

function never<T>() {
  return new Promise<T>(() => {});
}

function projectSurface(projectId: string) {
  return (
    <IssueSurface
      scope={{ type: "project", projectId }}
      modes={["list"]}
      renderHeader={() => null}
      renderLoading={() => <div data-testid="surface-loading" />}
      batchToolbar="never"
    />
  );
}

describe("IssueSurface — scope switch loading semantics", () => {
  let qc: QueryClient;

  beforeEach(() => {
    mockWsId.current = "ws-1";
    qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
    // p1 answers immediately with one issue; p2 stays in flight forever so
    // the test can observe the in-between state after switching.
    const listIssues = vi.fn((params?: ListIssuesParams) => {
      if (params?.project_id === "p2") return never<ListIssuesResponse>();
      const issues =
        params?.status === "todo" ? [makeIssue("i1", "P1 issue", "p1")] : [];
      return Promise.resolve({ issues, total: issues.length });
    });
    setApiInstance({
      listIssues,
      listGroupedIssues: vi.fn(() => never()),
      listProjects: vi.fn(() => never()),
      getAgentTaskSnapshot: vi.fn(() => never<AgentTask[]>()),
      getChildIssueProgress: vi.fn(() => never()),
    } as unknown as ApiClient);
    pruneIssueSurfaceViewStates([]);
  });

  afterEach(() => {
    cleanup();
    qc.clear();
    pruneIssueSurfaceViewStates([]);
    vi.restoreAllMocks();
  });

  it("shows loading — not the previous project's issues — while the next project is fetching", async () => {
    // Regression: the list queries use `placeholderData: keepPreviousData` to
    // keep sort/filter changes flicker-free WITHIN one surface. Without a
    // scope-keyed remount, that placeholder leaks ACROSS surfaces: switching
    // pinned projects kept rendering project A's cards (isLoading=false, so
    // no skeleton either) until project B's response landed — the "click does
    // nothing, then it snaps" bug.
    const { rerender } = render(
      <QueryClientProvider client={qc}>{projectSurface("p1")}</QueryClientProvider>,
    );

    await screen.findByText("P1 issue");

    rerender(
      <QueryClientProvider client={qc}>{projectSurface("p2")}</QueryClientProvider>,
    );

    // The switch must be honest: p2 has no data yet, so the surface is
    // loading — p1's cards must not impersonate p2.
    expect(screen.getByTestId("surface-loading")).toBeInTheDocument();
    expect(screen.queryByText("P1 issue")).not.toBeInTheDocument();
  });

  it("shows a cached project instantly on switch-back (no loading flash)", async () => {
    const { rerender } = render(
      <QueryClientProvider client={qc}>{projectSurface("p1")}</QueryClientProvider>,
    );
    await screen.findByText("P1 issue");

    rerender(
      <QueryClientProvider client={qc}>{projectSurface("p2")}</QueryClientProvider>,
    );
    expect(screen.getByTestId("surface-loading")).toBeInTheDocument();

    // Back to p1: its cache is warm, so the list renders immediately from
    // cache — remounting must not degrade the instant-switch path.
    rerender(
      <QueryClientProvider client={qc}>{projectSurface("p1")}</QueryClientProvider>,
    );
    await waitFor(() =>
      expect(screen.getByText("P1 issue")).toBeInTheDocument(),
    );
    expect(screen.queryByTestId("surface-loading")).not.toBeInTheDocument();
  });

  it("shows loading on a workspace switch even though the scope key is identical", async () => {
    // The workspace layout does NOT remount children on switch, and two
    // workspaces share the same scope key (e.g. "workspace:all") — so the
    // remount key must include wsId, or workspace A's issues impersonate
    // workspace B's while B is still fetching.
    //
    // A fresh element per render — reusing one element reference would let
    // React bail out of re-rendering the subtree entirely, and the wsId
    // change would never propagate.
    const workspaceSurface = () => (
      <IssueSurface
        scope={{ type: "workspace" }}
        modes={["list"]}
        renderHeader={() => null}
        renderLoading={() => <div data-testid="surface-loading" />}
        batchToolbar="never"
      />
    );

    const listIssues = vi.fn((params?: ListIssuesParams) => {
      const issues =
        params?.status === "todo" ? [makeIssue("i1", "WS1 issue", "p1")] : [];
      return Promise.resolve({ issues, total: issues.length });
    });
    setApiInstance({
      listIssues,
      listGroupedIssues: vi.fn(() => never()),
      listProjects: vi.fn(() => never()),
      getAgentTaskSnapshot: vi.fn(() => never<AgentTask[]>()),
      getChildIssueProgress: vi.fn(() => never()),
    } as unknown as ApiClient);

    const { rerender } = render(
      <QueryClientProvider client={qc}>{workspaceSurface()}</QueryClientProvider>,
    );
    await screen.findByText("WS1 issue");

    // Switch workspace: same scope, new wsId, and the new workspace's
    // fetches hang so the in-between state is observable.
    listIssues.mockImplementation(() => never<ListIssuesResponse>());
    mockWsId.current = "ws-2";
    rerender(
      <QueryClientProvider client={qc}>{workspaceSurface()}</QueryClientProvider>,
    );

    expect(screen.getByTestId("surface-loading")).toBeInTheDocument();
    expect(screen.queryByText("WS1 issue")).not.toBeInTheDocument();
  });
});
