import { cleanup, render, waitFor } from "@testing-library/react";
import * as React from "react";
import { afterEach, describe, expect, it, vi } from "vitest";

import {
  createToolcraftSourceAssetPresentationResources,
  type ToolcraftPresentationResourceHandle,
} from "../../source-assets/source-asset-presentation-resources";
import type { ToolcraftMediaAsset } from "../../state/types";
import {
  ToolcraftMediaPresentationProvider,
  useToolcraftMediaPresentationUrls,
} from "./toolcraft-media-presentation";

afterEach(cleanup);

function imageAsset(
  id: string,
  resourceRef = `media:image:${id}`,
): ToolcraftMediaAsset {
  return {
    assetKind: "image",
    fileName: `${id}.png`,
    id,
    layerId: `layer-${id}`,
    lifecycle: "ready",
    mimeType: "image/png",
    position: { x: 0, y: 0 },
    resourceRef,
  };
}

describe("Toolcraft media presentation context", () => {
  it("releases a resource that resolves after its consumer unmounts", async () => {
    let resolveRetain: ((handle: ToolcraftPresentationResourceHandle) => void) | undefined;
    const retain = vi.fn(
      () => new Promise<ToolcraftPresentationResourceHandle>((resolve) => {
        resolveRetain = resolve;
      }),
    );
    const release = vi.fn();

    function Probe(): React.JSX.Element {
      useToolcraftMediaPresentationUrls([
        {
          assetKind: "image",
          fileName: "image.png",
          id: "image",
          layerId: "layer",
          lifecycle: "ready",
          mimeType: "image/png",
          position: { x: 0, y: 0 },
          resourceRef: "media:image:test",
        },
      ]);
      return <span />;
    }

    const rendered = render(
      <ToolcraftMediaPresentationProvider
        bootstrapUrls={new Map()}
        resources={{ dispose: vi.fn(), retain }}
      >
        <Probe />
      </ToolcraftMediaPresentationProvider>,
    );

    expect(retain).toHaveBeenCalledTimes(1);
    rendered.unmount();
    if (!resolveRetain) {
      throw new Error("Expected retain to expose its pending resolver.");
    }
    resolveRetain({ release, url: "blob:test" });

    await waitFor(() => expect(release).toHaveBeenCalledTimes(1));
  });

  it("keeps a shared object URL retained when one logical asset is removed", async () => {
    const createObjectUrl = vi.fn(() => "blob:shared");
    const revokeObjectUrl = vi.fn();
    const releaseResourceRef = vi.fn();
    const resources = createToolcraftSourceAssetPresentationResources({
      createObjectUrl,
      resolveResource: vi.fn(async () => new Uint8Array([1, 2, 3])),
      retainResourceRef: vi.fn(() => releaseResourceRef),
      revokeObjectUrl,
    });
    const sharedRef = "media:image:shared";
    const first = imageAsset("first", sharedRef);
    const second = imageAsset("second", sharedRef);

    function Probe({
      assets,
    }: {
      assets: readonly ToolcraftMediaAsset[];
    }): React.JSX.Element {
      const urls = useToolcraftMediaPresentationUrls(assets);
      return <span data-count={urls.size} />;
    }

    const rendered = render(
      <ToolcraftMediaPresentationProvider
        bootstrapUrls={new Map()}
        resources={resources}
      >
        <Probe assets={[first, second]} />
      </ToolcraftMediaPresentationProvider>,
    );
    await waitFor(() =>
      expect(
        rendered.container.firstElementChild?.getAttribute("data-count"),
      ).toBe("2"),
    );

    rendered.rerender(
      <ToolcraftMediaPresentationProvider
        bootstrapUrls={new Map()}
        resources={resources}
      >
        <Probe assets={[first]} />
      </ToolcraftMediaPresentationProvider>,
    );
    await waitFor(() =>
      expect(
        rendered.container.firstElementChild?.getAttribute("data-count"),
      ).toBe("1"),
    );

    expect(createObjectUrl).toHaveBeenCalledTimes(1);
    expect(revokeObjectUrl).not.toHaveBeenCalled();
    rendered.unmount();
    expect(revokeObjectUrl).toHaveBeenCalledTimes(1);
    expect(releaseResourceRef).toHaveBeenCalledTimes(1);
  });
});
