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

import type { ToolcraftSourceAssetCoordinator } from "../../source-assets/source-asset-coordinator";
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);

function createDeferred<T>() {
  let reject!: (reason?: unknown) => void;
  const promise = new Promise<T>((_resolve, rejectPromise) => {
    reject = rejectPromise;
  });
  return { promise, reject };
}

describe("Toolcraft source asset provider ownership", () => {
  it("does not acquire or cache an owner for an uncommitted render", () => {
    const store = createTestStore();
    const coordinator = createTestCoordinator();
    const createCoordinator = vi.fn(() => coordinator);

    expect(renderToString(
      <ToolcraftSourceAssetProvider
        createCoordinator={createCoordinator}
        store={store}
      >
        <span>Server probe</span>
      </ToolcraftSourceAssetProvider>,
    )).toBe("");
    expect(createCoordinator).not.toHaveBeenCalled();

    const rendered = render(
      <ToolcraftSourceAssetProvider
        createCoordinator={createCoordinator}
        store={store}
      >
        <span data-testid="committed-source-asset-child" />
      </ToolcraftSourceAssetProvider>,
    );

    expect(rendered.getByTestId("committed-source-asset-child")).toBeDefined();
    expect(createCoordinator).toHaveBeenCalledTimes(1);
  });

  it("preserves the injected coordinator receiver through the provider", async () => {
    type ReceiverAwareCoordinator = ToolcraftSourceAssetCoordinator & {
      resolverBytes: Uint8Array;
    };

    const resolverBytes = new Uint8Array([4, 5, 6]);
    const coordinator: ReceiverAwareCoordinator = {
      ...createTestCoordinator(),
      resolverBytes,
      resolveResource: vi.fn(async function (
        this: ReceiverAwareCoordinator,
      ) {
        expect(this).toBe(coordinator);
        return this.resolverBytes;
      }),
    };
    const observed: ToolcraftSourceAssetCoordinator[] = [];
    render(
      <ToolcraftSourceAssetProvider
        createCoordinator={() => coordinator}
        store={createTestStore()}
      >
        <CoordinatorProbe capture={(value) => observed.push(value)} />
      </ToolcraftSourceAssetProvider>,
    );
    const resolveResource = observed[0]?.resolveResource;
    if (!resolveResource) throw new Error("Expected a resource resolver.");

    await expect(resolveResource(
      "media:image:receiver-aware",
      { signal: new AbortController().signal },
    )).resolves.toBe(resolverBytes);
  });

  it("reports hydration failures through the contextual owner channel", async () => {
    const failure = new Error("model hydration failed");
    const onError = vi.fn();
    const coordinator = createTestCoordinator({
      hydrateModels: vi.fn(async () => Promise.reject(failure)),
    });
    const rendered = render(
      <ToolcraftSourceAssetProvider
        createCoordinator={() => coordinator}
        onError={onError}
        store={createTestStore()}
      >
        <span />
      </ToolcraftSourceAssetProvider>,
    );

    await waitFor(() => expect(onError).toHaveBeenCalledWith(failure));
    rendered.unmount();
    expect(onError).toHaveBeenCalledTimes(1);
  });

  it("keeps same-store construction inputs stable across prop identity changes", async () => {
    const store = createTestStore();
    const firstReporter = vi.fn();
    const replacementReporter = vi.fn();
    const hydration = createDeferred<void>();
    const hydrationFailure = new Error("original owner hydration failed");
    const coordinator = createTestCoordinator({
      hydrateModels: vi.fn(() => hydration.promise),
    });
    const observed: ToolcraftSourceAssetCoordinator[] = [];
    const firstFactory = vi.fn(() => coordinator);
    const replacementFactory = vi.fn(() => createTestCoordinator());
    const rendered = render(
      <ToolcraftSourceAssetProvider
        createCoordinator={firstFactory}
        onError={firstReporter}
        store={store}
      >
        <CoordinatorProbe capture={(value) => observed.push(value)} />
        <span data-testid="stable-source-asset-child" />
      </ToolcraftSourceAssetProvider>,
    );

    rendered.rerender(
      <ToolcraftSourceAssetProvider
        createCoordinator={replacementFactory}
        onError={replacementReporter}
        store={store}
      >
        <CoordinatorProbe capture={(value) => observed.push(value)} />
        <span data-testid="stable-source-asset-child" />
      </ToolcraftSourceAssetProvider>,
    );

    expect(rendered.getByTestId("stable-source-asset-child")).toBeDefined();
    expect(observed.length).toBeGreaterThan(0);
    expect(observed.every((value) => value === coordinator)).toBe(true);
    expect(firstFactory).toHaveBeenCalledTimes(1);
    expect(replacementFactory).not.toHaveBeenCalled();
    hydration.reject(hydrationFailure);
    await waitFor(() => expect(firstReporter).toHaveBeenCalledWith(
      hydrationFailure,
    ));
    expect(replacementReporter).not.toHaveBeenCalled();
  });

  it("uses current construction inputs when the store changes", async () => {
    const firstStore = createTestStore();
    const secondStore = createTestStore();
    const firstReporter = vi.fn();
    const secondReporter = vi.fn();
    const secondFailure = new Error("second store hydration failed");
    const firstCoordinator = createTestCoordinator();
    const secondCoordinator = createTestCoordinator({
      hydrateModels: vi.fn(async () => Promise.reject(secondFailure)),
    });
    const firstFactory = vi.fn(() => firstCoordinator);
    const secondFactory = vi.fn(() => secondCoordinator);
    const rendered = render(
      <ToolcraftSourceAssetProvider
        createCoordinator={firstFactory}
        onError={firstReporter}
        store={firstStore}
      >
        <span data-testid="switched-source-asset-child" />
      </ToolcraftSourceAssetProvider>,
    );

    rendered.rerender(
      <ToolcraftSourceAssetProvider
        createCoordinator={secondFactory}
        onError={secondReporter}
        store={secondStore}
      >
        <span data-testid="switched-source-asset-child" />
      </ToolcraftSourceAssetProvider>,
    );

    await waitFor(() => expect(secondReporter).toHaveBeenCalledWith(
      secondFailure,
    ));
    expect(rendered.getByTestId("switched-source-asset-child")).toBeDefined();
    expect(firstFactory).toHaveBeenCalledWith(firstStore);
    expect(secondFactory).toHaveBeenCalledWith(secondStore);
    expect(firstReporter).not.toHaveBeenCalledWith(secondFailure);
  });

  it("supports the deprecated error reporter alias for all owner failures", async () => {
    const operationalFailure = new Error("model hydration failed through alias");
    const disposalFailure = new Error("owner disposal failed through alias");
    const onDisposeError = vi.fn();
    const coordinator = createTestCoordinator({
      dispose: vi.fn(async () => Promise.reject(disposalFailure)),
      hydrateModels: vi.fn(async () => Promise.reject(operationalFailure)),
    });
    const rendered = render(
      <ToolcraftSourceAssetProvider
        createCoordinator={() => coordinator}
        onDisposeError={onDisposeError}
        store={createTestStore()}
      >
        <span />
      </ToolcraftSourceAssetProvider>,
    );

    await waitFor(() => expect(onDisposeError).toHaveBeenCalledWith(
      operationalFailure,
    ));
    rendered.unmount();
    await waitFor(() => expect(onDisposeError).toHaveBeenCalledWith(
      disposalFailure,
    ));
  });

  it("prefers onError over the deprecated reporter alias", async () => {
    const failure = new Error("primary reporter failure");
    const onError = vi.fn();
    const onDisposeError = vi.fn();
    const rendered = render(
      <ToolcraftSourceAssetProvider
        createCoordinator={() => createTestCoordinator({
          hydrateModels: vi.fn(async () => Promise.reject(failure)),
        })}
        onDisposeError={onDisposeError}
        onError={onError}
        store={createTestStore()}
      >
        <span />
      </ToolcraftSourceAssetProvider>,
    );

    await waitFor(() => expect(onError).toHaveBeenCalledWith(failure));
    expect(onDisposeError).not.toHaveBeenCalled();
    rendered.unmount();
  });
});
