import { act, render, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";

import { encodeToolcraftModelDocument } from "../../model-import/canonical/model-document-codec";
import { createValidModelDocument } from "../../model-import/canonical/model-document-test-support";
import { defineToolcraft } from "../../schema/define-toolcraft";
import type {
  ToolcraftModelPresentationConsumerDeclaration,
  ToolcraftModelPresentationLease,
  ToolcraftModelResourceResolver,
} from "./model-render-binding";
import {
  useToolcraftModelPresentationConsumer,
  type ToolcraftModelPresentationConsumer,
} from "./model-presentation-consumer";
import { resolveToolcraftModelPresentationMode } from "./model-presentation-mode";
import { ToolcraftModelRenderProvider } from "./model-render-provider";

const declaration = Object.freeze({
  id: "product-model",
  sourceTarget: "source.model",
});

function ConsumerProbe({
  capture,
  requestedDeclaration = declaration,
}: {
  capture: (consumer: ToolcraftModelPresentationConsumer) => void;
  requestedDeclaration?: ToolcraftModelPresentationConsumerDeclaration;
}): null {
  capture(useToolcraftModelPresentationConsumer(requestedDeclaration));
  return null;
}

function createResolver(): ToolcraftModelResourceResolver {
  const bytes = encodeToolcraftModelDocument(createValidModelDocument());
  return vi.fn(async () => bytes);
}

describe("Toolcraft model presentation mode", () => {
  const schema = defineToolcraft({
    canvas: { enabled: true },
    panels: {
      controls: {
        sections: [{
          controls: {
            model: {
              assetKind: "model",
              target: "source.model",
              type: "fileDrop",
            },
            orientation: {
              defaultValue: { position: [0, 0, 5], up: [0, 1, 0] },
              target: "view.orientation",
              type: "orientationGizmo",
            },
          },
          title: "Model",
        }],
        title: "Controls",
      },
    },
  });

  it("defaults to runtime presentation and validates custom declarations", () => {
    expect(resolveToolcraftModelPresentationMode(schema)).toEqual({
      mode: "runtime",
    });
    expect(resolveToolcraftModelPresentationMode(schema, {
      consumers: [{
        ...declaration,
        orientationTarget: "view.orientation",
      }],
      mode: "custom",
    })).toEqual({
      consumers: [{
        ...declaration,
        orientationTarget: "view.orientation",
      }],
      mode: "custom",
    });
  });

  it("rejects duplicate, unknown, untrimmed, and mismatched declarations", () => {
    expect(() => resolveToolcraftModelPresentationMode(schema, {
      consumers: [declaration, { ...declaration, sourceTarget: "source.other" }],
      mode: "custom",
    })).toThrow(/consumer id.*unique/iu);
    expect(() => resolveToolcraftModelPresentationMode(schema, {
      consumers: [declaration, { ...declaration, id: "other" }],
      mode: "custom",
    })).toThrow(/source target.*unique/iu);
    expect(() => resolveToolcraftModelPresentationMode(schema, {
      consumers: [{ ...declaration, id: " product-model" }],
      mode: "custom",
    })).toThrow(/trimmed/iu);
    expect(() => resolveToolcraftModelPresentationMode(schema, {
      consumers: [{ ...declaration, sourceTarget: "source.unknown" }],
      mode: "custom",
    })).toThrow(/exactly one model fileDrop/iu);
    expect(() => resolveToolcraftModelPresentationMode(schema, {
      consumers: [{ ...declaration, orientationTarget: "view.other" }],
      mode: "custom",
    })).toThrow(/orientationGizmo/iu);
  });
});

describe("custom model presentation consumer", () => {
  it("registers an exact declaration and clears feedback after acquisition", async () => {
    const clearPresentationFeedback = vi.fn();
    const reportPresentationFeedback = vi.fn();
    let consumer: ToolcraftModelPresentationConsumer | undefined;
    const view = render(
      <ToolcraftModelRenderProvider
        activeCustomTargets={["source.model"]}
        clearPresentationFeedback={clearPresentationFeedback}
        customConsumers={[declaration]}
        reportPresentationFeedback={reportPresentationFeedback}
        resolveResource={createResolver()}
      >
        <ConsumerProbe capture={(value) => { consumer = value; }} />
      </ToolcraftModelRenderProvider>,
    );
    await waitFor(() => expect(consumer).toBeDefined());
    await waitFor(() => {
      expect(clearPresentationFeedback).toHaveBeenCalledWith("source.model");
    });

    let lease: ToolcraftModelPresentationLease | undefined;
    await act(async () => {
      lease = await consumer!.acquirePresentation("document:model", {
        purpose: "preview",
        signal: new AbortController().signal,
      });
    });

    expect(lease?.root.getObjectByName("primitive-1")).toBeDefined();
    expect(reportPresentationFeedback).not.toHaveBeenCalled();
    lease?.release();
    view.unmount();
  });

  it("publishes typed feedback for a missing consumer and acquisition failure", async () => {
    const reportMissing = vi.fn();
    const missing = render(
      <ToolcraftModelRenderProvider
        activeCustomTargets={["source.model"]}
        customConsumers={[declaration]}
        reportPresentationFeedback={reportMissing}
        resolveResource={createResolver()}
      >
        <span />
      </ToolcraftModelRenderProvider>,
    );
    await waitFor(() => {
      expect(reportMissing).toHaveBeenCalledWith(
        "source.model",
        expect.objectContaining({
          category: "resource-unavailable",
          code: "model-presentation-consumer-missing",
        }),
      );
    });
    missing.unmount();

    let consumer: ToolcraftModelPresentationConsumer | undefined;
    const reportUnavailable = vi.fn();
    const unavailable = render(
      <ToolcraftModelRenderProvider
        activeCustomTargets={["source.model"]}
        customConsumers={[declaration]}
        reportPresentationFeedback={reportUnavailable}
        resolveResource={vi.fn(async () => null)}
      >
        <ConsumerProbe capture={(value) => { consumer = value; }} />
      </ToolcraftModelRenderProvider>,
    );
    await waitFor(() => expect(consumer).toBeDefined());
    await expect(consumer!.acquirePresentation("document:missing", {
      purpose: "preview",
      signal: new AbortController().signal,
    })).rejects.toThrow(/unavailable/iu);
    expect(reportUnavailable).toHaveBeenCalledWith(
      "source.model",
      expect.objectContaining({
        category: "resource-unavailable",
        code: "model-presentation-unavailable",
      }),
    );
    unavailable.unmount();
  });

  it("rejects a consumer hook declaration that differs from composition", () => {
    expect(() => render(
      <ToolcraftModelRenderProvider
        customConsumers={[declaration]}
        resolveResource={createResolver()}
      >
        <ConsumerProbe
          capture={() => undefined}
          requestedDeclaration={{ ...declaration, sourceTarget: "source.other" }}
        />
      </ToolcraftModelRenderProvider>,
    )).toThrow(/does not match/iu);
  });

  it("requires an oriented consumer to match the visible orientation gizmo", () => {
    const oriented = {
      ...declaration,
      orientationTarget: "view.orientation",
    };
    expect(() => render(
      <ToolcraftModelRenderProvider
        customConsumers={[oriented]}
        resolveResource={createResolver()}
        visibleOrientationTarget="view.other"
      >
        <ConsumerProbe
          capture={() => undefined}
          requestedDeclaration={oriented}
        />
      </ToolcraftModelRenderProvider>,
    )).toThrow(/visible orientationGizmo/iu);
  });
});
