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

import { defineToolcraft } from "../../schema/define-toolcraft";
import { createHydrationModelAsset } from "../../model-import/model-persistence-hydration-test-support";
import { createToolcraftState } from "../../state/create-template-state";
import { createToolcraftExternalStore } from "../../state/toolcraft-external-store";
import type { ToolcraftSourceAssetCoordinator } from "../../source-assets/source-asset-coordinator";
import { useOptionalToolcraftModelRenderHost } from "../model-rendering/model-render-provider";
import { ToolcraftModelPresentationModeContext } from "../model-rendering/model-presentation-mode";
import { ToolcraftRoot } from "./toolcraft-root";
import { ToolcraftSourceAssetProvider } from "./toolcraft-source-asset-context";
import {
  ToolcraftSourceAssetCoordinatorProbe as CoordinatorProbe,
  createTestSourceAssetCoordinator as createTestCoordinator,
  createTestSourceAssetStore as createTestStore,
} from "./toolcraft-source-asset-test-support";

afterEach(() => {
  cleanup();
  vi.unstubAllGlobals();
});

function ModelRenderHostProbe(): React.JSX.Element {
  const host = useOptionalToolcraftModelRenderHost();
  return <span data-testid="model-render-host">{host ? "ready" : "missing"}</span>;
}

describe("Toolcraft source asset provider", () => {
  it("shares one app-owned coordinator and disposes it on unmount", async () => {
    const schema = defineToolcraft({
      canvas: { enabled: true, upload: true },
      panels: {},
    });
    const coordinators: ToolcraftSourceAssetCoordinator[] = [];
    const capture = (coordinator: ToolcraftSourceAssetCoordinator) => {
      coordinators.push(coordinator);
    };
    const rendered = render(
      <ToolcraftRoot schema={schema}>
        <CoordinatorProbe capture={capture} />
        <CoordinatorProbe capture={capture} />
      </ToolcraftRoot>,
    );

    expect(coordinators).toHaveLength(2);
    expect(coordinators[0]).toBe(coordinators[1]);
    const dispose = vi.spyOn(coordinators[0] as ToolcraftSourceAssetCoordinator, "dispose");

    rendered.unmount();

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

  it("creates one stable coordinator owner under React StrictMode", async () => {
    const store = createTestStore();
    const coordinator = createTestCoordinator();
    const createCoordinator = vi.fn(() => coordinator);
    const coordinators: ToolcraftSourceAssetCoordinator[] = [];
    const rendered = render(
      <React.StrictMode>
        <ToolcraftSourceAssetProvider
          createCoordinator={createCoordinator}
          store={store}
        >
          <CoordinatorProbe capture={(value) => coordinators.push(value)} />
          <span data-testid="strict-source-asset-child" />
        </ToolcraftSourceAssetProvider>
      </React.StrictMode>,
    );

    expect(createCoordinator).toHaveBeenCalledTimes(1);
    expect(createCoordinator).toHaveBeenCalledWith(store);
    expect(coordinators.length).toBeGreaterThanOrEqual(2);
    expect(coordinators.every((value) => value === coordinator)).toBe(true);
    expect(rendered.getByTestId("strict-source-asset-child")).toBeDefined();

    rendered.unmount();
    await waitFor(() => expect(coordinator.dispose).toHaveBeenCalledTimes(1));
  });

  it("connects the production resource resolver to the standard model render host", () => {
    const coordinator = {
      ...createTestCoordinator(),
      resolveResource: vi.fn(async () => null),
    } satisfies ToolcraftSourceAssetCoordinator;
    const rendered = render(
      <ToolcraftSourceAssetProvider
        createCoordinator={() => coordinator}
        store={createTestStore()}
      >
        <ModelRenderHostProbe />
      </ToolcraftSourceAssetProvider>,
    );

    expect(rendered.getByTestId("model-render-host").textContent).toBe("ready");
  });

  it("scopes unavailable-resource browser proof to one real resolver resource and cleans up", async () => {
    const resourceRef = `media:image:sha256:${"c".repeat(64)}`;
    const store = createToolcraftExternalStore(
      createToolcraftState(
        defineToolcraft({ canvas: { enabled: true }, panels: {} }),
        {
          mediaAssets: [
            {
              assetKind: "image",
              fileName: "fixture.png",
              id: "fixture-image",
              layerId: "fixture-layer",
              lifecycle: "ready",
              mimeType: "image/png",
              position: { x: 0, y: 0 },
              resourceRef,
              size: { height: 20, unit: "px", width: 20 },
              sourceTarget: "source.images",
            },
          ],
        },
      ),
    );
    const baseResolveResource = vi.fn(async () => new Uint8Array([1, 2, 3]));
    const coordinator = {
      ...createTestCoordinator(),
      resolveResource: baseResolveResource,
    } satisfies ToolcraftSourceAssetCoordinator;
    let proofCoordinator: ToolcraftSourceAssetCoordinator | null = null;
    const rendered = render(
      <div data-slot="toolcraft-runtime-app">
        <ToolcraftSourceAssetProvider
          createCoordinator={() => coordinator}
          store={store}
        >
          <CoordinatorProbe capture={(value) => {
            proofCoordinator = value;
          }} />
        </ToolcraftSourceAssetProvider>
      </div>,
    );
    const request = {
      fileName: "fixture.png",
      requestId: "proof-request",
      sourceTarget: "source.images",
    };

    document.dispatchEvent(
      new CustomEvent(
        "toolcraft.browser-proof.unavailable-resource-request",
        { detail: { ...request, action: "activate" } },
      ),
    );

    await waitFor(() => {
      expect(store.getCommittedState().mediaAssets[0]).toMatchObject({
        lifecycle: "unavailable",
      });
      expect(
        rendered.container
          .querySelector('[data-slot="toolcraft-runtime-app"]')
          ?.getAttribute("data-toolcraft-unavailable-resource-evidence"),
      ).toContain('"status":"active"');
    });
    await expect(
      proofCoordinator!.resolveResource!(
        resourceRef,
        { signal: new AbortController().signal },
      ),
    ).resolves.toBeNull();

    document.dispatchEvent(
      new CustomEvent(
        "toolcraft.browser-proof.unavailable-resource-request",
        { detail: { ...request, action: "cleanup" } },
      ),
    );

    await waitFor(() => {
      expect(store.getCommittedState().mediaAssets[0]).toMatchObject({
        lifecycle: "ready",
      });
      expect(
        rendered.container
          .querySelector('[data-slot="toolcraft-runtime-app"]')
          ?.getAttribute("data-toolcraft-unavailable-resource-evidence"),
      ).toContain('"status":"cleaned"');
    });
    await expect(
      proofCoordinator!.resolveResource!(
        resourceRef,
        { signal: new AbortController().signal },
      ),
    ).resolves.toEqual(new Uint8Array([1, 2, 3]));
    expect(baseResolveResource).toHaveBeenCalledTimes(3);
  });

  it("reports a committed custom model whose declared consumer is not mounted", async () => {
    const schema = defineToolcraft({
      canvas: { enabled: true },
      panels: {
        controls: {
          sections: [{
            controls: {
              model: {
                assetKind: "model",
                target: "source.model",
                type: "fileDrop",
              },
            },
            title: "Model",
          }],
          title: "Controls",
        },
      },
    });
    const store = createToolcraftExternalStore(createToolcraftState(schema));
    const { id: _id, layerId: _layerId, ...model } =
      createHydrationModelAsset("document:model", {
        lifecycle: "clean",
        sourceTarget: "source.model",
      });
    store.dispatch({
      assets: [model],
      replaceExisting: true,
      type: "media.importBatch",
    });
    const coordinator = {
      ...createTestCoordinator(),
      resolveResource: vi.fn(async () => null),
    } satisfies ToolcraftSourceAssetCoordinator;
    const rendered = render(
      <ToolcraftModelPresentationModeContext.Provider value={{
        consumers: [{ id: "product-model", sourceTarget: "source.model" }],
        mode: "custom",
      }}>
        <ToolcraftSourceAssetProvider
          createCoordinator={() => coordinator}
          store={store}
        >
          <span />
        </ToolcraftSourceAssetProvider>
      </ToolcraftModelPresentationModeContext.Provider>,
    );

    await waitFor(() => {
      expect(coordinator.reportPresentationFeedback).toHaveBeenCalledWith(
        "source.model",
        expect.objectContaining({
          code: "model-presentation-consumer-missing",
        }),
      );
    });
    rendered.unmount();
  });

  it("reconciles model hydration when default attachments are removed and reset", async () => {
    const schema = defineToolcraft({
      canvas: { enabled: true, upload: true },
      media: {
        defaultAssets: [{
          assetKind: "model",
          fileName: "default.glb",
          sourceFiles: [{
            dataUrl: "data:model/gltf-binary;base64,AAAA",
            path: "default.glb",
          }],
          sourceTarget: "source.model",
        }],
      },
      panels: {
        controls: {
          sections: [{
            controls: {
              source: {
                assetKind: "model",
                defaultValue: null,
                target: "source.model",
                type: "fileDrop",
              },
            },
            title: "Model",
          }],
          title: "Controls",
        },
      },
    });
    const store = createToolcraftExternalStore(createToolcraftState(schema));
    const coordinator = createTestCoordinator();
    const rendered = render(
      <ToolcraftSourceAssetProvider
        createCoordinator={() => coordinator}
        store={store}
      >
        <CoordinatorProbe capture={() => undefined} />
      </ToolcraftSourceAssetProvider>,
    );

    await waitFor(() => expect(coordinator.hydrateModels).toHaveBeenCalledTimes(1));
    expect(coordinator.clearPresentationFeedback).toHaveBeenLastCalledWith(
      "source.model",
    );
    const defaultAssetId = store.getCommittedState().mediaAssets[0]?.id;
    if (!defaultAssetId) throw new Error("Expected a default model attachment.");
    store.dispatch({ mediaId: defaultAssetId, type: "media.delete" });
    await waitFor(() => expect(coordinator.hydrateModels).toHaveBeenCalledTimes(2));
    expect(coordinator.clearPresentationFeedback).toHaveBeenCalledTimes(2);
    expect(coordinator.clearPresentationFeedback).toHaveBeenLastCalledWith(
      "source.model",
    );
    store.dispatch({ type: "controls.reset" });
    await waitFor(() => expect(coordinator.hydrateModels).toHaveBeenCalledTimes(3));
    expect(coordinator.clearPresentationFeedback).toHaveBeenCalledTimes(3);
    expect(coordinator.clearPresentationFeedback).toHaveBeenLastCalledWith(
      "source.model",
    );

    rendered.unmount();
  });

  it("rehydrates a default image after reset restores its attachment", async () => {
    const schema = defineToolcraft({
      canvas: { enabled: true },
      media: {
        defaultAssets: [{
          dataUrl: "data:image/png;base64,AQID",
          fileName: "default.png",
          id: "default-image",
          layerId: "default-layer",
          mimeType: "image/png",
          sourceTarget: "source.image",
        }],
      },
      panels: {
        controls: {
          sections: [{
            controls: {
              source: {
                defaultValue: null,
                target: "source.image",
                type: "fileDrop",
              },
            },
            title: "Source",
          }],
          title: "Controls",
        },
      },
    });
    const store = createToolcraftExternalStore(createToolcraftState(schema));
    const rendered = render(
      <ToolcraftSourceAssetProvider store={store}>
        <span />
      </ToolcraftSourceAssetProvider>,
    );

    await waitFor(() => {
      expect(store.getCommittedState().mediaAssets[0]).toMatchObject({
        lifecycle: "ready",
      });
    });
    store.dispatch({ mediaId: "default-image", type: "media.delete" });
    store.dispatch({ type: "controls.reset" });
    expect(store.getCommittedState().mediaAssets[0]).toMatchObject({
      lifecycle: "restoring",
    });

    await waitFor(() => {
      expect(store.getCommittedState().mediaAssets[0]).toMatchObject({
        lifecycle: "ready",
      });
    });

    rendered.unmount();
  });

  it("reports asynchronous disposal failure without an unhandled rejection", async () => {
    const store = createTestStore();
    const failure = new AggregateError(
      [new Error("rollback failed")],
      "cleanup failed",
    );
    const coordinator = createTestCoordinator({
      dispose: vi.fn(async () => Promise.reject(failure)),
    });
    const onError = vi.fn();
    const onUnhandledRejection = vi.fn();
    window.addEventListener("unhandledrejection", onUnhandledRejection);
    const rendered = render(
      <ToolcraftSourceAssetProvider
        createCoordinator={() => coordinator}
        onError={onError}
        store={store}
      >
        <CoordinatorProbe capture={() => undefined} />
      </ToolcraftSourceAssetProvider>,
    );

    rendered.unmount();
    await waitFor(() => expect(onError).toHaveBeenCalledWith(failure));
    await Promise.resolve();
    window.removeEventListener("unhandledrejection", onUnhandledRejection);

    expect(onUnhandledRejection).not.toHaveBeenCalled();
  });

  it("uses the runtime reportError boundary by default", async () => {
    const failure = new Error("repository owner disposal failed");
    const reportError = vi.fn();
    vi.stubGlobal("reportError", reportError);
    const coordinator = createTestCoordinator({
      dispose: vi.fn(async () => Promise.reject(failure)),
    });
    const rendered = render(
      <ToolcraftSourceAssetProvider
        createCoordinator={() => coordinator}
        store={createTestStore()}
      >
        <CoordinatorProbe capture={() => undefined} />
      </ToolcraftSourceAssetProvider>,
    );

    rendered.unmount();

    await waitFor(() => expect(reportError).toHaveBeenCalledWith(failure));
  });
});
