import { act, renderHook } from "@testing-library/react";
import type * as React from "react";
import { describe, expect, it, vi } from "vitest";

import {
  createToolcraftRendererPipelineRuntime,
  registerToolcraftRendererPipeline,
  type ToolcraftRendererPipelinePassContract,
} from "../../rendering";
import { ToolcraftPipelineContext } from "./toolcraft-pipeline-context";
import { useToolcraftPipelinePass } from "./use-toolcraft-pipeline-pass";

function createDeferred<T>() {
  let reject!: (error: unknown) => void;
  let resolve!: (value: T) => void;
  const promise = new Promise<T>((resolvePromise, rejectPromise) => {
    reject = rejectPromise;
    resolve = resolvePromise;
  });
  return { promise, reject, resolve };
}

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

describe("useToolcraftPipelinePass", () => {
  it("bridges a synchronous pipeline result into the initial committed state", () => {
    const registration = createRegistration();
    const runtime = createToolcraftRendererPipelineRuntime(registration);
    const compute = vi.fn(() => "model-a");
    const wrapper = ({ children }: { children: React.ReactNode }) => (
      <ToolcraftPipelineContext.Provider value={runtime}>
        {children}
      </ToolcraftPipelineContext.Provider>
    );

    const { result } = renderHook(
      () =>
        useToolcraftPipelinePass(
          registration.getPass("preview"),
          { value: "a" },
          compute,
        ),
      { wrapper },
    );

    expect(result.current).toEqual({
      result: "model-a",
      status: "success",
    });
    expect(compute).toHaveBeenCalledTimes(1);
  });

  it("does not restart a pass when an inline compute callback is recreated by its result render", () => {
    const registration = createRegistration();
    const runtime = createToolcraftRendererPipelineRuntime(registration);
    let computeCalls = 0;
    let renders = 0;
    const wrapper = ({ children }: { children: React.ReactNode }) => (
      <ToolcraftPipelineContext.Provider value={runtime}>
        {children}
      </ToolcraftPipelineContext.Provider>
    );

    const { result } = renderHook(
      () => {
        renders += 1;
        return useToolcraftPipelinePass(
          registration.getPass("preview"),
          { value: "inline" },
          () => {
            computeCalls += 1;
            return "model-inline";
          },
        );
      },
      { wrapper },
    );

    expect(result.current).toEqual({
      result: "model-inline",
      status: "success",
    });
    expect(computeCalls).toBe(1);
    expect(renders).toBeLessThanOrEqual(2);
    expect(runtime.getSnapshot().passes.preview.executions).toBe(1);
  });

  it("computes synchronously without a registered pipeline client", () => {
    const registration = createRegistration();
    const compute = () => "fallback-model";

    const { result } = renderHook(() =>
      useToolcraftPipelinePass(
        registration.getPass("preview"),
        { value: "fallback" },
        compute,
      ),
    );

    expect(result.current).toEqual({
      result: "fallback-model",
      status: "success",
    });
  });

  it("suppresses stale async results after request identity changes", async () => {
    const registration = createRegistration();
    const first = createDeferred<string>();
    const second = createDeferred<string>();
    const { result, rerender } = renderHook(
      ({ cacheInput, compute }) =>
        useToolcraftPipelinePass(
          registration.getPass("preview"),
          cacheInput,
          compute,
        ),
      {
        initialProps: {
          cacheInput: { value: "a" },
          compute: () => first.promise,
        },
      },
    );

    rerender({
      cacheInput: { value: "b" },
      compute: () => second.promise,
    });
    await act(async () => first.resolve("stale-model"));
    expect(result.current.status).toBe("pending");

    await act(async () => second.resolve("current-model"));
    expect(result.current).toEqual({
      result: "current-model",
      status: "success",
    });
  });

  it("returns an explicit error result for rejected work", async () => {
    const registration = createRegistration();
    const failure = new Error("render failed");
    const compute = () => Promise.reject(failure);
    const { result } = renderHook(() =>
      useToolcraftPipelinePass(
        registration.getPass("preview"),
        { value: "failure" },
        compute,
      ),
    );

    await act(() => Promise.resolve());

    expect(result.current).toEqual({
      error: failure,
      status: "error",
    });
  });

  it("keeps the resolved model when compute identity changes without descriptor changes", async () => {
    const registration = createRegistration();
    const runtime = createToolcraftRendererPipelineRuntime(registration);
    const firstCompute = vi.fn(() => "cached-model");
    const secondCompute = vi.fn(() => "unexpected-model");
    const wrapper = ({ children }: { children: React.ReactNode }) => (
      <ToolcraftPipelineContext.Provider value={runtime}>
        {children}
      </ToolcraftPipelineContext.Provider>
    );
    const { result, rerender } = renderHook(
      ({ cacheInput, compute }) =>
        useToolcraftPipelinePass(
          registration.getPass("preview"),
          cacheInput,
          compute,
        ),
      {
        initialProps: {
          cacheInput: { value: "same" },
          compute: firstCompute,
        },
        wrapper,
      },
    );

    rerender({
      cacheInput: { value: "same" },
      compute: secondCompute,
    });
    await act(() => Promise.resolve());

    expect(result.current).toEqual({
      result: "cached-model",
      status: "success",
    });
    expect(firstCompute).toHaveBeenCalledTimes(1);
    expect(secondCompute).not.toHaveBeenCalled();
    expect(runtime.getSnapshot().passes.preview.cacheHits).toBe(0);
    expect(runtime.getSnapshot().passes.preview.executions).toBe(1);
  });
});
