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

const instrumentation = vi.hoisted(() => ({
  ownerFactories: vi.fn(),
  runtimeInitializers: vi.fn(),
}));

vi.mock("../../rendering/renderer-pipeline-runtime", async (importOriginal) => {
  const actual =
    await importOriginal<
      typeof import("../../rendering/renderer-pipeline-runtime")
    >();
  return {
    ...actual,
    createToolcraftRendererPipelineRuntime: (
      ...args: Parameters<typeof actual.createToolcraftRendererPipelineRuntime>
    ) => {
      instrumentation.runtimeInitializers();
      return actual.createToolcraftRendererPipelineRuntime(...args);
    },
  };
});

vi.mock("../../rendering", async (importOriginal) => {
  const actual = await importOriginal<typeof import("../../rendering")>();
  return {
    ...actual,
    createToolcraftRendererPipelineRuntimeOwner: (
      ...args: Parameters<
        typeof actual.createToolcraftRendererPipelineRuntimeOwner
      >
    ) => {
      instrumentation.ownerFactories();
      return actual.createToolcraftRendererPipelineRuntimeOwner(...args);
    },
  };
});

import {
  registerToolcraftRendererPipeline,
  type ToolcraftRendererPipelinePassContract,
} from "../../rendering/renderer-pipeline-registration";
import type { ToolcraftRendererPipelineClient } from "../../rendering/renderer-pipeline-runtime";
import { createToolcraftRendererPipelineRuntimeOwner } from "../../rendering/renderer-pipeline-runtime-owner";
import { defineToolcraft } from "../../schema/define-toolcraft";
import { ToolcraftApp } from "./toolcraft-app";
import { useToolcraftPipeline } from "./use-toolcraft-pipeline";

afterEach(cleanup);

beforeEach(() => {
  instrumentation.ownerFactories.mockClear();
  instrumentation.runtimeInitializers.mockClear();
});

const schema = defineToolcraft({
  canvas: { enabled: true },
  panels: {
    controls: {
      sections: [],
      title: "Controls",
    },
  },
});

function createRegistration(runtimeId = "pipeline-initialization-test-v1") {
  return registerToolcraftRendererPipeline<{
    preview: ToolcraftRendererPipelinePassContract<string>;
  }>()({
    interactionInvalidation: [],
    passes: [
      {
        id: "preview",
        inputs: [],
        invalidatedBy: [],
        kind: "composite",
        lifecycle: { cache: "none", resourceScope: "call" },
        output: "preview",
        quality: "full",
        runsOn: "main",
      },
    ],
    runtimeId,
  });
}

describe("Toolcraft pipeline initialization", () => {
  it("reads a stable uninitialized snapshot without constructing a runtime", async () => {
    const owner = createToolcraftRendererPipelineRuntimeOwner(createRegistration());

    const first = owner.client.getSnapshot();
    const second = owner.client.getSnapshot();

    expect(instrumentation.runtimeInitializers).not.toHaveBeenCalled();
    expect(second).toBe(first);
    expect(first).toMatchObject({
      disposed: false,
      runtimeId: "pipeline-initialization-test-v1",
    });
    expect(first.passes.preview.executions).toBe(0);
    expect(Object.isFrozen(first)).toBe(true);
    expect(Object.isFrozen(first.passes)).toBe(true);
    expect(Object.isFrozen(first.passes.preview)).toBe(true);

    await owner.dispose();
    const disposed = owner.client.getSnapshot();
    expect(instrumentation.runtimeInitializers).not.toHaveBeenCalled();
    expect(disposed.disposed).toBe(true);
    expect(owner.client.getSnapshot()).toBe(disposed);
  });

  it("initializes one real runtime after a StrictMode commit and disposes it", async () => {
    const observedRuntimes: ToolcraftRendererPipelineClient[] = [];

    function RuntimeProbe(): null {
      const runtime = useToolcraftPipeline();
      if (runtime) {
        runtime.getSnapshot();
        observedRuntimes.push(runtime);
      }
      return null;
    }

    const view = render(
      <React.StrictMode>
        <ToolcraftApp
          canvasContent={<RuntimeProbe />}
          rendererPipelineRegistration={createRegistration()}
          schema={schema}
        />
      </React.StrictMode>,
    );
    await act(() => Promise.resolve());

    expect(instrumentation.runtimeInitializers).toHaveBeenCalledTimes(1);
    const runtime = observedRuntimes.at(-1)!;
    expect(runtime.getSnapshot().disposed).toBe(false);

    view.unmount();
    await act(async () => {
      await Promise.resolve();
      await Promise.resolve();
    });

    expect(runtime.getSnapshot().disposed).toBe(true);
  });

  it("allocates no pipeline provider owner or runtime for a neutral app", () => {
    let runtime: ToolcraftRendererPipelineClient | null | undefined;

    function RuntimeProbe(): null {
      runtime = useToolcraftPipeline();
      return null;
    }

    render(<ToolcraftApp canvasContent={<RuntimeProbe />} schema={schema} />);

    expect(runtime).toBeNull();
    expect(instrumentation.ownerFactories).not.toHaveBeenCalled();
    expect(instrumentation.runtimeInitializers).not.toHaveBeenCalled();
  });

  it("keeps independently mounted app runtimes isolated", async () => {
    const runtimes: ToolcraftRendererPipelineClient[] = [];

    function RuntimeProbe(): null {
      const runtime = useToolcraftPipeline();
      if (runtime && !runtimes.includes(runtime)) {
        runtimes.push(runtime);
      }
      return null;
    }

    const view = render(
      <>
        <ToolcraftApp
          canvasContent={<RuntimeProbe />}
          rendererPipelineRegistration={createRegistration()}
          schema={schema}
        />
        <ToolcraftApp
          canvasContent={<RuntimeProbe />}
          rendererPipelineRegistration={createRegistration()}
          schema={schema}
        />
      </>,
    );
    await act(() => Promise.resolve());

    expect(instrumentation.runtimeInitializers).toHaveBeenCalledTimes(2);
    expect(runtimes).toHaveLength(2);
    expect(runtimes[0]).not.toBe(runtimes[1]);

    view.unmount();
    await act(() => Promise.resolve());
    expect(runtimes.every((runtime) => runtime.getSnapshot().disposed)).toBe(true);
  });

  it("reopens the same provider facade after an Activity effect reconnect", async () => {
    const registration = createRegistration("activity-reconnect-v1");
    const clients: ToolcraftRendererPipelineClient[] = [];

    function RuntimeProbe(): null {
      const client = useToolcraftPipeline();
      if (client) {
        clients.push(client);
      }
      return null;
    }

    const renderActivity = (mode: "hidden" | "visible") => (
      <React.Activity mode={mode}>
        <ToolcraftApp
          canvasContent={<RuntimeProbe />}
          rendererPipelineRegistration={registration}
          schema={schema}
        />
      </React.Activity>
    );
    const view = render(renderActivity("visible"));
    await act(() => Promise.resolve());
    const client = clients.at(-1)!;

    view.rerender(renderActivity("hidden"));
    await act(async () => {
      await Promise.resolve();
      await Promise.resolve();
    });
    expect(client.getSnapshot().disposed).toBe(true);

    view.rerender(renderActivity("visible"));
    await act(() => Promise.resolve());
    expect(clients.at(-1)).toBe(client);
    expect(client.getSnapshot().disposed).toBe(false);
    expect(instrumentation.runtimeInitializers).toHaveBeenCalledTimes(2);

    view.unmount();
    await act(async () => {
      await Promise.resolve();
      await Promise.resolve();
    });
    expect(client.getSnapshot().disposed).toBe(true);
  });

  it("replaces the owner when registration identity changes", async () => {
    const firstRegistration = createRegistration("pipeline-first-v1");
    const secondRegistration = createRegistration("pipeline-second-v1");
    const clients: ToolcraftRendererPipelineClient[] = [];

    function RuntimeProbe(): null {
      const client = useToolcraftPipeline();
      if (client && !clients.includes(client)) {
        clients.push(client);
      }
      return null;
    }

    const view = render(
      <ToolcraftApp
        canvasContent={<RuntimeProbe />}
        rendererPipelineRegistration={firstRegistration}
        schema={schema}
      />,
    );
    await act(() => Promise.resolve());
    const firstClient = clients[0]!;

    view.rerender(
      <ToolcraftApp
        canvasContent={<RuntimeProbe />}
        rendererPipelineRegistration={secondRegistration}
        schema={schema}
      />,
    );
    await act(async () => {
      await Promise.resolve();
      await Promise.resolve();
    });
    const secondClient = clients[1]!;

    expect(secondClient).not.toBe(firstClient);
    expect(firstClient.getSnapshot().disposed).toBe(true);
    await expect(
      firstClient.runPass(firstRegistration.getPass("preview"), undefined, () => "stale"),
    ).rejects.toThrow(
      'Renderer pipeline runtime owner "pipeline-first-v1" is disposed.',
    );
    expect(secondClient.getSnapshot()).toMatchObject({
      disposed: false,
      runtimeId: "pipeline-second-v1",
    });
    await expect(
      secondClient.runPass(firstRegistration.getPass("preview"), undefined, () => "stale"),
    ).rejects.toThrow(
      'Renderer pipeline pass "preview" does not belong to registration "pipeline-second-v1".',
    );
    expect(instrumentation.runtimeInitializers).toHaveBeenCalledTimes(2);
  });
});
