// @vitest-environment happy-dom
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { downloadBlob, downloadTextFile } from "./download";

// Capture the real appendChild before any spy replaces it, so the
// happy-dom implementation is preserved when the appendChild spy calls
// through (otherwise removeChild inside downloadTextFile can't find the
// node we appended).
const originalAppendChild = document.body.appendChild.bind(document.body);

/**
 * `downloadTextFile` drives a hidden `<a download>` and clicks it. The
 * observable side effects we care about:
 *
 *   1. An `<a>` element with the right `href` (object URL) and `download`
 *      (filename) is appended to `document.body`.
 *   2. The element is removed from the DOM (cleanup).
 *   3. The object URL is revoked (memory hygiene).
 *
 * We don't try to assert on the browser's actual download behavior — that's
 * not observable from the test environment. We assert on the contract the
 * implementation honors.
 */
describe("downloadTextFile", () => {
  let createSpy: ReturnType<typeof vi.spyOn>;
  let revokeSpy: ReturnType<typeof vi.spyOn>;

  beforeEach(() => {
    createSpy = vi
      .spyOn(URL, "createObjectURL")
      .mockReturnValue("blob:test-url");
    revokeSpy = vi.spyOn(URL, "revokeObjectURL");
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  it("test_appends_anchor_with_object_url_and_filename_then_cleans_up", async () => {
    // Spy on appendChild to capture the anchor that's about to be appended
    // (downloadTextFile removes it again synchronously, so lastElementChild
    // is back to null by the time the call returns).
    const captured: HTMLAnchorElement[] = [];
    const appendSpy = vi
      .spyOn(document.body, "appendChild")
      .mockImplementation((node) => {
        if (node instanceof HTMLAnchorElement) captured.push(node);
        // Preserve the real append side-effect so removeChild later in
        // downloadTextFile can find the node.
        return originalAppendChild.call(document.body, node);
      });

    downloadTextFile("claim-CLM-1.x12", "text/x12", "ISA*00*~");

    expect(captured).toHaveLength(1);
    const a = captured[0];
    expect(a.tagName).toBe("A");
    expect(a.getAttribute("download")).toBe("claim-CLM-1.x12");
    expect(a.getAttribute("href")).toBe("blob:test-url");
    expect(a.style.display).toBe("none");

    // removeChild ran synchronously inside downloadTextFile — the anchor
    // is no longer a child of document.body.
    expect(document.body.contains(a)).toBe(false);

    expect(appendSpy).toHaveBeenCalledTimes(1);
    expect(createSpy).toHaveBeenCalledTimes(1);
    // The revoke is scheduled via queueMicrotask — flush the microtask
    // queue so the assertion sees it.
    await Promise.resolve();
    expect(revokeSpy).toHaveBeenCalledWith("blob:test-url");
  });

  it("test_blob_carries_supplied_mime_type_and_text_bytes", () => {
    downloadTextFile("a.txt", "text/plain", "hello world");

    expect(createSpy).toHaveBeenCalledTimes(1);
    const blob = createSpy.mock.calls[0][0] as Blob;
    expect(blob.type).toBe("text/plain;charset=utf-8");
    expect(blob.size).toBe("hello world".length);
  });

  it("test_noop_when_document_undefined", () => {
    // SSR-style guard: with no `document`, the function returns silently
    // instead of throwing.
    const originalDocument = globalThis.document;
    // @ts-expect-error - intentionally strip document for this case
    delete (globalThis as { document?: Document }).document;
    try {
      expect(() =>
        downloadTextFile("a.txt", "text/plain", "x"),
      ).not.toThrow();
    } finally {
      (globalThis as { document?: Document }).document = originalDocument;
    }
  });

  it("test_download_blob_uses_supplied_blob_and_filename", async () => {
    // The blob variant exists for binary payloads (ZIP, PDF, ...) where
    // the caller already has a Blob and just wants the browser to save
    // it. The anchor's href must point at the supplied blob's object
    // URL and the download attribute must be the filename.
    const captured: HTMLAnchorElement[] = [];
    const appendSpy = vi
      .spyOn(document.body, "appendChild")
      .mockImplementation((node) => {
        if (node instanceof HTMLAnchorElement) captured.push(node);
        return originalAppendChild.call(document.body, node);
      });

    const blob = new Blob(["PK\u0003\u0004fake-zip"], {
      type: "application/zip",
    });
    downloadBlob("resubmit-2-claims.zip", blob);

    expect(captured).toHaveLength(1);
    const a = captured[0];
    expect(a.getAttribute("download")).toBe("resubmit-2-claims.zip");
    expect(a.getAttribute("href")).toBe("blob:test-url");
    expect(a.style.display).toBe("none");

    expect(appendSpy).toHaveBeenCalledTimes(1);
    expect(createSpy).toHaveBeenCalledTimes(1);
    await Promise.resolve();
    expect(revokeSpy).toHaveBeenCalledWith("blob:test-url");
  });
});
