/**
 * SSR + hydration tests.
 *
 * Each case renders a screen to HTML with `renderToString` (server pass, UTC
 * clock), injects that HTML into the DOM, then hydrates it with `hydrateRoot`
 * (client pass, Asia/Dhaka clock) and fails if React reports any hydration
 * mismatch — either through `onRecoverableError` or a hydration `console.error`.
 */
import { act } from "react";
import { renderToString } from "react-dom/server";
import { hydrateRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { AppShellFallback, PageFallback } from "@/components/RouteFallback";
import { formatDate, formatDateTime, formatMoney } from "@/lib/format";

// --- module mocks -----------------------------------------------------------

vi.mock("@tanstack/react-router", () => ({
  createFileRoute: () => (options: unknown) => ({ options }),
  Link: ({ children, ...rest }: React.ComponentProps<"a">) => <a {...rest}>{children}</a>,
  useRouterState: () => "/",
  Outlet: () => null,
  ClientOnly: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));

const BALANCES = [
  {
    account_id: "a1",
    code: "1010",
    name: "Main Cash",
    category: "asset",
    total_debit: 0,
    total_credit: 0,
    balance: 0,
  },
  {
    account_id: "a2",
    code: "4010",
    name: "Studio Rental Revenue",
    category: "revenue",
    total_debit: 0,
    total_credit: 0,
    balance: 0,
  },
];

vi.mock("@/lib/erp", () => ({
  listAccountBalances: vi.fn(async () => BALANCES),
  listJournalEntries: vi.fn(async () => []),
}));

vi.mock("@/components/RowActions", () => ({
  printHtmlDocument: vi.fn(),
}));

// --- harness ----------------------------------------------------------------

type Mismatch = { source: string; detail: string };

/**
 * `renderToString` and `innerHTML` escape a few characters differently (e.g.
 * `>` inside attribute values), which is a serializer artifact rather than a
 * hydration difference.
 */
function normalizeHtml(html: string) {
  return html.replace(/&gt;/g, ">").replace(/&#x27;/g, "'");
}

function makeClient() {
  const client = new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Infinity } } });
  client.setQueryData(["account-balances"], BALANCES);
  client.setQueryData(["journal-entries"], []);
  return client;
}

let mismatches: Mismatch[] = [];
let originalError: typeof console.error;

beforeEach(() => {
  mismatches = [];
  originalError = console.error;
  console.error = (...args: unknown[]) => {
    const text = args.map((a) => (a instanceof Error ? a.message : String(a))).join(" ");
    if (/[Hh]ydrat|did not match|server rendered/.test(text)) {
      mismatches.push({ source: "console.error", detail: text });
    }
  };
});

afterEach(() => {
  console.error = originalError;
  vi.clearAllMocks();
});

async function ssrThenHydrate(node: React.ReactElement) {
  const client = makeClient();
  const tree = <QueryClientProvider client={client}>{node}</QueryClientProvider>;

  const serverHtml = renderToString(tree);
  expect(serverHtml.length).toBeGreaterThan(0);

  const container = document.createElement("div");
  container.innerHTML = serverHtml;
  document.body.appendChild(container);

  let root: ReturnType<typeof hydrateRoot> | undefined;
  await act(async () => {
    root = hydrateRoot(container, tree, {
      onRecoverableError: (error) => {
        mismatches.push({ source: "onRecoverableError", detail: String(error) });
      },
    });
  });

  const clientHtml = container.innerHTML;
  await act(async () => {
    root?.unmount();
  });
  container.remove();

  return { serverHtml, clientHtml };
}

// Route components are read off the route definition returned by createFileRoute.
async function routeComponent(path: string) {
  const mod = (await import(path)) as { Route: { options: { component: React.ComponentType } } };
  return mod.Route.options.component;
}

// --- tests ------------------------------------------------------------------

describe("SSR fallback UI", () => {
  it("hydrates the page fallback without mismatches", async () => {
    const { serverHtml, clientHtml } = await ssrThenHydrate(<PageFallback />);
    expect(mismatches).toEqual([]);
    expect(normalizeHtml(clientHtml)).toBe(normalizeHtml(serverHtml));
  });

  it("hydrates the app shell fallback and is never blank", async () => {
    const { serverHtml, clientHtml } = await ssrThenHydrate(<AppShellFallback />);
    expect(mismatches).toEqual([]);
    expect(serverHtml).toContain("Loading workspace");
    expect(serverHtml).toBe(clientHtml);
  });
});

describe("authenticated route SSR hydration", () => {
  const routes: Array<[string, string, string]> = [
    ["Balance verification", "@/routes/_authenticated/accounting_.verify-reset", "Every account has a zero balance"],
    ["Trial balance", "@/routes/_authenticated/accounting_.trial-balance", "Main Cash"],
  ];

  for (const [label, path, marker] of routes) {
    it(`${label} hydrates without mismatches`, async () => {
      const Component = await routeComponent(path);
      const { serverHtml, clientHtml } = await ssrThenHydrate(<Component />);
      expect(mismatches).toEqual([]);
      // Same rendered output on both passes, and real content (not a blank shell).
      expect(normalizeHtml(clientHtml)).toBe(normalizeHtml(serverHtml));
      expect(serverHtml).toContain(marker);
    });
  }
});

describe("date and money rendering is clock/locale independent", () => {
  it("renders identical markup on the server and after hydration", async () => {
    const Fixture = () => (
      <ul>
        <li>{formatDate("2026-02-05")}</li>
        <li>{formatDateTime("2026-02-05T23:45:00Z")}</li>
        <li>{formatMoney(1234567.891)}</li>
      </ul>
    );
    const { serverHtml, clientHtml } = await ssrThenHydrate(<Fixture />);
    expect(mismatches).toEqual([]);
    expect(normalizeHtml(clientHtml)).toBe(normalizeHtml(serverHtml));
    expect(serverHtml).toContain("05 Feb 2026, 23:45");
  });
});
