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

import { defineToolcraft } from "../../schema/define-toolcraft";
import type { ToolcraftSourceAssetCoordinator } from "../../source-assets/source-asset-coordinator";
import { ControlsPanel } from "../controls-panel/controls-panel";
import { useToolcraftStore } from "./toolcraft-store-context";
import { ToolcraftRoot } from "./toolcraft-root";
import { ToolcraftSourceAssetCoordinatorContext } from "./toolcraft-source-asset-context";
import { runtimePublicApiSchema } from "./runtime-public-api-test-support";
import {
  useToolcraft,
  useToolcraftDispatch,
  useToolcraftSelector,
} from "./use-toolcraft";

const runtimePublicApiPanelActionSchema = defineToolcraft({
  canvas: { enabled: true },
  panels: {
    controls: {
      sections: [
        {
          actionGroup: "secondary",
          controls: {
            action: {
              actions: [
                {
                  label: "Resolve media",
                  value: "resolve-media",
                  variant: "default",
                },
              ],
              target: "panel.actions",
              type: "panelActions",
            },
          },
        },
      ],
      title: "Controls",
    },
  },
});

function createRuntimePublicApiSourceAssetCoordinator(
  resolveResource?: ToolcraftSourceAssetCoordinator["resolveResource"],
): ToolcraftSourceAssetCoordinator {
  const idleOperation = Object.freeze({
    phase: "idle" as const,
    target: "source",
  });

  return {
    cancelTarget: vi.fn(),
    clearPresentationFeedback: vi.fn(),
    dispose: vi.fn(async () => undefined),
    getOperation: () => idleOperation,
    getPresentation: () => null,
    hydrateBinaryMedia: vi.fn(async () => undefined),
    hydrateModels: vi.fn(async () => undefined),
    importBatch: vi.fn(async () => ({
      assetIds: [] as const,
      kind: "committed" as const,
    })),
    repairModel: vi.fn(async () => ({ kind: "cancelled" as const })),
    reportPresentationFeedback: vi.fn(),
    ...(resolveResource ? { resolveResource } : {}),
    subscribe: () => () => undefined,
  };
}

afterEach(cleanup);

describe("Toolcraft template runtime public API integration", () => {
  it("binds the source asset resolver into panel action context", async () => {
    const bytes = new Uint8Array([11, 22, 33]);
    const resolveResource = vi.fn<
      NonNullable<ToolcraftSourceAssetCoordinator["resolveResource"]>
    >(async () => bytes);
    const coordinator = createRuntimePublicApiSourceAssetCoordinator(
      resolveResource,
    );
    const signal = new AbortController().signal;
    let resolvedBytes: Uint8Array | null | undefined;

    render(
      <ToolcraftRoot schema={runtimePublicApiPanelActionSchema}>
        <ToolcraftSourceAssetCoordinatorContext.Provider value={coordinator}>
          <ControlsPanel
            framed={false}
            onPanelAction={async ({ resolveMediaResource }) => {
              resolvedBytes = await resolveMediaResource("media:known", {
                signal,
              });
            }}
          />
        </ToolcraftSourceAssetCoordinatorContext.Provider>
      </ToolcraftRoot>,
    );

    fireEvent.click(screen.getByRole("button", { name: "Resolve media" }));

    await waitFor(() => expect(resolvedBytes).toEqual(bytes));
    expect(resolveResource).toHaveBeenCalledWith("media:known", { signal });
  });

  it("returns null from panel action media resolution when no resolver exists", async () => {
    const coordinator = createRuntimePublicApiSourceAssetCoordinator();
    const signal = new AbortController().signal;
    let resolvedBytes: Uint8Array | null | undefined;

    render(
      <ToolcraftRoot schema={runtimePublicApiPanelActionSchema}>
        <ToolcraftSourceAssetCoordinatorContext.Provider value={coordinator}>
          <ControlsPanel
            framed={false}
            onPanelAction={async ({ resolveMediaResource }) => {
              resolvedBytes = await resolveMediaResource("media:missing", {
                signal,
              });
            }}
          />
        </ToolcraftSourceAssetCoordinatorContext.Provider>
      </ToolcraftRoot>,
    );

    fireEvent.click(screen.getByRole("button", { name: "Resolve media" }));

    await waitFor(() => expect(resolvedBytes).toBeNull());
  });

  it("keeps broad consumers live while isolating selector and dispatch consumers", () => {
    const renders = {
      broad: 0,
      dispatch: 0,
      playback: 0,
      value: 0,
      viewport: 0,
    };

    function BroadProbe() {
      const { state } = useToolcraft();

      renders.broad += 1;

      return <output data-testid="broad-time">{state.timeline.currentTimeSeconds}</output>;
    }

    function DispatchProbe() {
      const dispatch = useToolcraftDispatch();

      renders.dispatch += 1;

      return (
        <button
          type="button"
          onClick={() =>
            dispatch({
              target: "selectedLayer.opacity",
              type: "controls.setValue",
              value: 50,
            })
          }
        >
          Set value
        </button>
      );
    }

    function ValueProbe() {
      const value = useToolcraftSelector(
        (state) => state.values["selectedLayer.opacity"],
      );

      renders.value += 1;

      return <output data-testid="selected-value">{String(value)}</output>;
    }

    function PlaybackProbe() {
      const currentTimeSeconds = useToolcraftSelector(
        (state) => state.timeline.currentTimeSeconds,
      );

      renders.playback += 1;

      return <output data-testid="playback-time">{currentTimeSeconds}</output>;
    }

    function ViewportProbe() {
      const offset = useToolcraftSelector(
        (state) => ({ ...state.canvas.offset }),
        (previous, next) => previous.x === next.x && previous.y === next.y,
      );

      renders.viewport += 1;

      return <output data-testid="viewport-offset">{offset.x},{offset.y}</output>;
    }

    function TransientProbe() {
      const store = useToolcraftStore();

      return (
        <div>
          <button
            type="button"
            onClick={() =>
              store.dispatchTransient({
                currentTimeSeconds: 2,
                type: "timeline.setCurrentTime",
              })
            }
          >
            Set playback time
          </button>
          <button
            type="button"
            onClick={() =>
              store.dispatchTransient({
                offset: { x: 0, y: 0 },
                type: "canvas.setViewport",
                zoom: 150,
              })
            }
          >
            Set viewport zoom
          </button>
        </div>
      );
    }

    render(
      <ToolcraftRoot schema={runtimePublicApiSchema}>
        <BroadProbe />
        <DispatchProbe />
        <ValueProbe />
        <PlaybackProbe />
        <ViewportProbe />
        <TransientProbe />
      </ToolcraftRoot>,
    );

    expect(renders).toEqual({
      broad: 1,
      dispatch: 1,
      playback: 1,
      value: 1,
      viewport: 1,
    });

    fireEvent.click(screen.getByRole("button", { name: "Set value" }));

    expect(screen.getByTestId("selected-value").textContent).toBe("50");
    expect(renders).toEqual({
      broad: 2,
      dispatch: 1,
      playback: 1,
      value: 2,
      viewport: 1,
    });

    fireEvent.click(screen.getByRole("button", { name: "Set playback time" }));

    expect(screen.getByTestId("broad-time").textContent).toBe("2");
    expect(screen.getByTestId("playback-time").textContent).toBe("2");
    expect(renders).toEqual({
      broad: 3,
      dispatch: 1,
      playback: 2,
      value: 2,
      viewport: 1,
    });

    fireEvent.click(screen.getByRole("button", { name: "Set viewport zoom" }));

    expect(screen.getByTestId("viewport-offset").textContent).toBe("0,0");
    expect(renders).toEqual({
      broad: 4,
      dispatch: 1,
      playback: 2,
      value: 2,
      viewport: 1,
    });
  });
});
