/**
 * CE-WP-0005-T08 — full create → annotate → export → reimport E2E.
 *
 * Walks the demo loop end-to-end:
 *
 *   1.  Load app → empty state.
 *   2.  Create session "Demo" via the inline name input.
 *   3.  Upload a synthetic PDF.
 *   4.  Inject a selection for the manifest's known-good quote → save
 *       evidence with a commentary.
 *   5.  (Sanity) Click Export → Copy as Markdown; assert the clipboard
 *       payload contains the quote + commentary + openContextUrl.
 *   6.  Click Export ZIP; capture the Blob via a URL.createObjectURL
 *       spy.
 *   7.  Click Import ZIP with the captured Blob; assert merge:
 *       - one document (deduped by fingerprint)
 *       - two evidence rows (additive merge)
 *   8.  Click Export → Copy as Markdown on the merged evidence; assert
 *       the citation card text matches the original.
 */

// @vitest-environment happy-dom

import { act, cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import type { Document, DocumentRepresentation } from "@shared/document";
import type { DocumentId, RepresentationId } from "@shared/ids";
import type { Selector } from "@shared/selector";
import type { PdfSelectionCapture } from "@anchor/index";
import manifest from "../../fixtures/pdfs/manifest.json" with { type: "json" };

// ---------------------------------------------------------------------------
// Mocks
// ---------------------------------------------------------------------------

interface ViewerProps {
  pdfUrl: string;
  storedAnnotations: readonly { id: string; text: string; selectors: readonly Selector[] }[];
  scrollToAnnotationId?: string;
  onSelectionCaptured(capture: PdfSelectionCapture, selectors: Selector[]): void;
}

interface ViewerSnapshot {
  pdfUrl: string | null;
  onSelectionCaptured: ViewerProps["onSelectionCaptured"] | null;
}

const viewerSnapshot: ViewerSnapshot = {
  pdfUrl: null,
  onSelectionCaptured: null,
};

vi.mock("@anchor/index", async (importOriginal) => {
  const original = await importOriginal<typeof import("@anchor/index")>();
  const MockPdfSpikeViewer = (props: ViewerProps) => {
    viewerSnapshot.pdfUrl = props.pdfUrl;
    viewerSnapshot.onSelectionCaptured = props.onSelectionCaptured;
    return <div data-testid="mock-pdf-viewer" />;
  };
  return { ...original, PdfSpikeViewer: MockPdfSpikeViewer };
});

const FIXTURE = manifest.fixtures.find((f) => f.id === "fristsetzung-bezifferung")!;
const SYNTHETIC_CANONICAL = ["Pre.", FIXTURE.known_good_quote, "Post."].join(" ");

// Bypass real PDF.js extraction. The mock returns a stable fingerprint
// so the merge-on-fingerprint dedup logic actually fires on re-import.
vi.mock("@source/index", async (importOriginal) => {
  const original = await importOriginal<typeof import("@source/index")>();
  let counter = 0;
  return {
    ...original,
    ingestPdfFromFile: vi.fn(
      async (file: File | Blob, store: import("@source/index").PdfByteStore) => {
        counter += 1;
        const filename =
          "name" in file && typeof file.name === "string" ? file.name : "uploaded.pdf";
        const documentId = ("doc_e2e_" + counter) as DocumentId;
        const representationId = ("rep_e2e_" + counter) as RepresentationId;
        const bytes = new Uint8Array(await file.arrayBuffer());
        const record = store.put(documentId, bytes);
        const document: Document = {
          id: documentId,
          mediaType: "application/pdf",
          title: filename,
          uri: record.blobUrl,
          // Stable fingerprint — the importer dedupes by fingerprint, so
          // re-importing the export merges into the same doc.
          fingerprint: "fp-stable-e2e",
          createdAt: "2026-05-25T00:00:00.000Z",
          updatedAt: "2026-05-25T00:00:00.000Z",
        };
        const representation: DocumentRepresentation = {
          id: representationId,
          documentId,
          representationType: "pdf-text",
          contentHash: "fp-stable-e2e",
          canonicalText: SYNTHETIC_CANONICAL,
          pageMap: [{ page: 1, width: 595, height: 842 }],
          offsetMap: [
            {
              page: 1,
              globalStart: 0,
              globalEnd: SYNTHETIC_CANONICAL.length,
              pageLength: SYNTHETIC_CANONICAL.length,
            },
          ],
          generatedAt: "2026-05-25T00:00:00.000Z",
        };
        return { document, representation };
      },
    ),
  };
});

// ---------------------------------------------------------------------------
// Test-time intercepts: clipboard, URL.createObjectURL, file-picker input.
// ---------------------------------------------------------------------------

function syntheticCaptureFor(text: string, page: number): PdfSelectionCapture {
  return {
    kind: "pdf",
    text,
    page,
    rects: [{ x: 0.1, y: 0.2, width: 0.4, height: 0.04 }],
    boundingRect: { x: 0.1, y: 0.2, width: 0.4, height: 0.04 },
  };
}

let writeText: ReturnType<typeof vi.fn>;
function installClipboardSpy() {
  writeText = vi.fn(async () => undefined);
  const proto = Object.getPrototypeOf(navigator.clipboard);
  Object.defineProperty(proto, "writeText", {
    configurable: true,
    writable: true,
    value: writeText,
  });
}

interface UrlInterceptor {
  blobs: Blob[];
  restore(): void;
}
function installUrlInterceptor(): UrlInterceptor {
  const blobs: Blob[] = [];
  const origCreate = URL.createObjectURL;
  const origRevoke = URL.revokeObjectURL;
  let counter = 0;
  URL.createObjectURL = ((b: Blob) => {
    blobs.push(b);
    counter += 1;
    return `blob:test-${counter}`;
  }) as typeof URL.createObjectURL;
  URL.revokeObjectURL = (() => {}) as typeof URL.revokeObjectURL;
  return {
    blobs,
    restore() {
      URL.createObjectURL = origCreate;
      URL.revokeObjectURL = origRevoke;
    },
  };
}

interface FilePickerInterceptor {
  inputs: HTMLInputElement[];
  restore(): void;
}
function installFilePickerInterceptor(): FilePickerInterceptor {
  const inputs: HTMLInputElement[] = [];
  const origCreate = document.createElement.bind(document);
  (document.createElement as unknown) = (tag: string) => {
    const el = origCreate(tag);
    if (tag === "input") inputs.push(el as HTMLInputElement);
    return el;
  };
  return {
    inputs,
    restore() {
      document.createElement = origCreate;
    },
  };
}

async function loadApp() {
  const { App } = await import("@app/App");
  return render(<App />);
}

// ---------------------------------------------------------------------------
// Test
// ---------------------------------------------------------------------------

describe("CE-WP-0005-T08 — full create → annotate → export → reimport E2E", () => {
  let url: UrlInterceptor;
  let picker: FilePickerInterceptor;

  beforeEach(() => {
    viewerSnapshot.pdfUrl = null;
    viewerSnapshot.onSelectionCaptured = null;
    globalThis.localStorage?.clear();
    if (typeof window !== "undefined") {
      history.replaceState(null, "", window.location.pathname);
    }
    url = installUrlInterceptor();
    picker = installFilePickerInterceptor();
  });

  afterEach(() => {
    cleanup();
    url.restore();
    picker.restore();
    vi.restoreAllMocks();
  });

  it(
    "creates session, annotates, exports ZIP, re-imports ZIP, and round-trips the citation card",
    { timeout: 30000 },
    async () => {
      const user = userEvent.setup();
      await loadApp();

      // Step 1: empty state.
      await screen.findByTestId("empty-state");

      // Step 2: create session "Demo".
      const nameInput = screen.getByTestId("empty-state-input");
      await user.type(nameInput, "Demo");
      await user.click(screen.getByTestId("empty-state-create"));

      // Wait for the active session UI (collection list + upload dropzone).
      await screen.findByTestId("upload-dropzone");

      // Step 3: upload a synthetic PDF.
      installClipboardSpy();
      const fileBytes = new Uint8Array([0x25, 0x50, 0x44, 0x46, 0x2d]); // %PDF-
      const pdfFile = new File([fileBytes], "e2e.pdf", { type: "application/pdf" });
      const uploadInput = screen.getByTestId("upload-file-input") as HTMLInputElement;
      await user.upload(uploadInput, pdfFile);

      // Wait for the viewer to mount with the uploaded doc.
      await waitFor(() => expect(viewerSnapshot.onSelectionCaptured).not.toBeNull());

      // Step 4: inject a selection + save evidence with commentary.
      await act(async () => {
        viewerSnapshot.onSelectionCaptured!(
          syntheticCaptureFor(FIXTURE.known_good_quote, FIXTURE.known_good_quote_page),
          [{ type: "TextQuoteSelector", exact: FIXTURE.known_good_quote }],
        );
      });
      await user.type(
        screen.getByPlaceholderText(/Add a one-line comment/),
        "E2E session commentary",
      );
      await user.click(screen.getByRole("button", { name: /Save evidence/ }));
      await screen.findByText(/E2E session commentary/);

      // Step 5 (sanity): export the evidence as Markdown via the sidebar.
      installClipboardSpy();
      await user.click(await screen.findByLabelText("Export evidence item"));
      await user.click(await screen.findByRole("menuitem", { name: "Copy as Markdown" }));
      await waitFor(() => expect(writeText).toHaveBeenCalled());
      const firstCard = writeText.mock.calls[0]![0] as string;
      expect(firstCard).toContain(FIXTURE.known_good_quote);
      expect(firstCard).toContain("E2E session commentary");
      expect(firstCard).toMatch(/\/viewer\?document=doc_[^&]+&annotation=ann_[^)]+/);

      // Step 6: click Export ZIP from the session menu and capture the
      // generated Blob via the URL.createObjectURL spy.
      const urlCountBeforeExport = url.blobs.length;
      await user.click(screen.getByTestId("session-menu-toggle"));
      await user.click(await screen.findByTestId("session-menu-export"));
      await waitFor(() => expect(url.blobs.length).toBeGreaterThan(urlCountBeforeExport));
      // The exported ZIP is the most-recently-minted blob URL'd to <a download>.
      const zipBlob = url.blobs[url.blobs.length - 1]!;
      expect(zipBlob.size).toBeGreaterThan(0);

      // Step 7: import the ZIP back via the menu. Intercept the file
      // input the App opens, set our captured blob as its file, fire
      // change.
      const inputsBefore = picker.inputs.length;
      await user.click(screen.getByTestId("session-menu-toggle"));
      await user.click(await screen.findByTestId("session-menu-import"));
      // The handler created a new file input.
      await waitFor(() => expect(picker.inputs.length).toBeGreaterThan(inputsBefore));
      const importInput = picker.inputs[picker.inputs.length - 1]!;
      const zipFile = new File([await zipBlob.arrayBuffer()], "demo.zip", {
        type: "application/zip",
      });
      // happy-dom's `files` is a getter on HTMLInputElement.prototype;
      // a value-override on the instance silently doesn't take effect.
      // Replace with a property getter that returns our fake FileList.
      const fakeFileList = {
        0: zipFile,
        length: 1,
        item: (i: number) => (i === 0 ? zipFile : null),
        [Symbol.iterator]: function* () {
          yield zipFile;
        },
      };
      Object.defineProperty(importInput, "files", {
        configurable: true,
        get: () => fakeFileList,
      });
      await act(async () => {
        if (typeof importInput.onchange === "function") {
          importInput.onchange(new Event("change"));
        }
        // Yield so the import promise gets a chance to start.
        await new Promise((r) => setTimeout(r, 0));
      });

      // Wait for the merge to manifest in the DOM. ADR-0008's additive
      // policy means we should now see TWO evidence rows for the same
      // commentary (the original + the imported copy), and the
      // collection should still show ONE document (deduped by
      // fingerprint).
      await waitFor(
        () => {
          const matches = screen.queryAllByText(/E2E session commentary/);
          expect(matches.length).toBeGreaterThanOrEqual(2);
        },
        { timeout: 12000 },
      );

      await waitFor(() => {
        const items = screen
          .getByTestId("collection-list-items")
          .querySelectorAll("li");
        expect(items.length).toBe(1);
      });

      // Step 8: export the *merged* evidence as Markdown and assert the
      // round-trip preserves quote + commentary + URL shape.
      installClipboardSpy();
      const toggles = screen.getAllByLabelText("Export evidence item");
      // Use the last evidence row (the one from the import) just to
      // distinguish from the original.
      await user.click(toggles[toggles.length - 1]!);
      await user.click(await screen.findByRole("menuitem", { name: "Copy as Markdown" }));
      await waitFor(() => expect(writeText).toHaveBeenCalled());
      const mergedCard = writeText.mock.calls[0]![0] as string;
      expect(mergedCard).toContain(FIXTURE.known_good_quote);
      expect(mergedCard).toContain("E2E session commentary");
      expect(mergedCard).toMatch(/\/viewer\?document=doc_[^&]+&annotation=ann_[^)]+/);
    },
  );
});
