import { act, type ReactElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { useHarnessSelection, type HarnessSelectionValue } from './use-harness-selection';

function json(body: unknown, status = 200): Response {
  return new Response(JSON.stringify(body), {
    status,
    headers: { 'Content-Type': 'application/json' },
  });
}

interface Scenario {
  harnesses?: unknown;
  catalog?: { body: unknown; status?: number };
  selection?: unknown;
  /** When set, the PUT resolves only when this is called (for race tests). */
  deferPut?: boolean;
}

interface Deferred<T> {
  promise: Promise<T>;
  resolve: (value: T) => void;
}

function defer<T>(): Deferred<T> {
  let resolve!: (value: T) => void;
  const promise = new Promise<T>((r) => {
    resolve = r;
  });
  return { promise, resolve };
}

let putBodies: unknown[] = [];
let putDeferred: Deferred<Response> | null = null;

function installFetch(scenario: Scenario): ReturnType<typeof vi.fn> {
  putBodies = [];
  putDeferred = scenario.deferPut ? defer<Response>() : null;
  const fetchMock = vi.fn(async (input: unknown, init?: RequestInit) => {
    const url = String(input);
    const method = String(init?.method ?? 'GET').toUpperCase();
    if (url === '/api/harnesses') return json(scenario.harnesses ?? []);
    if (url.startsWith('/api/harnesses/') && url.endsWith('/catalog')) {
      const spec = scenario.catalog ?? {
        body: { harnessId: 'pi', version: '1', fingerprint: 'f', models: [] },
      };
      return json(spec.body, spec.status ?? 200);
    }
    if (url === '/api/chat/preferences/selection' && method === 'GET') {
      return json({ selection: scenario.selection ?? null });
    }
    if (url === '/api/chat/preferences/selection' && method === 'PUT') {
      putBodies.push(JSON.parse(String(init?.body)));
      const ok = json({ selection: JSON.parse(String(init?.body)) });
      if (putDeferred) return putDeferred.promise;
      return ok;
    }
    return new Response('not found', { status: 404 });
  });
  vi.stubGlobal('fetch', fetchMock);
  return fetchMock;
}

let latest: HarnessSelectionValue | null = null;

function Probe(): ReactElement | null {
  latest = useHarnessSelection();
  return null;
}

let root: Root | null;
let container: HTMLElement;

beforeAll(() => {
  Object.defineProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT', {
    configurable: true,
    value: true,
  });
});

afterAll(() => {
  Reflect.deleteProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT');
});

beforeEach(() => {
  latest = null;
  container = document.createElement('div');
  document.body.append(container);
  root = createRoot(container);
});

afterEach(async () => {
  await act(async () => {
    root?.unmount();
  });
  document.body.replaceChildren();
  vi.unstubAllGlobals();
});

async function mount(): Promise<void> {
  await act(async () => {
    root?.render(<Probe />);
  });
  await flush();
}

async function flush(times = 5): Promise<void> {
  for (let i = 0; i < times; i += 1) {
    await act(async () => {
      await Promise.resolve();
    });
  }
}

function value(): HarnessSelectionValue {
  if (!latest) throw new Error('hook value not captured');
  return latest;
}

const PI_CATALOG = {
  harnessId: 'pi',
  version: '2026-08-11',
  fingerprint: 'fp',
  models: [
    {
      harnessId: 'pi',
      providerId: 'openai',
      modelId: 'gpt-5',
      displayName: 'GPT-5',
      reasoningCapability: true,
      inputTypes: ['text'],
      authState: 'ready',
      availability: 'available',
    },
    {
      harnessId: 'pi',
      providerId: 'anthropic',
      modelId: 'claude',
      displayName: 'Claude',
      reasoningCapability: true,
      inputTypes: ['text'],
      authState: 'ready',
      availability: 'available',
    },
  ],
};

describe('useHarnessSelection', () => {
  it('loads harnesses and, once a harness is chosen, the model options come only from its catalog', async () => {
    installFetch({
      harnesses: [{ id: 'pi', displayName: 'Pi', capabilities: [] }],
      catalog: { body: PI_CATALOG },
      selection: null,
    });
    await mount();

    expect(value().harnesses).toEqual([{ id: 'pi', displayName: 'Pi', capabilities: [] }]);
    expect(value().catalog).toBeNull();

    await act(async () => {
      value().selectHarness('pi');
    });
    await flush();

    expect(value().catalog?.harnessId).toBe('pi');
    expect(value().catalog?.models.map((m) => m.modelId)).toEqual(['gpt-5', 'claude']);
  });

  it('does not auto-select any catalog row when there is no persisted selection (no first-row fallback)', async () => {
    const fetchMock = installFetch({
      harnesses: [{ id: 'pi', displayName: 'Pi', capabilities: [] }],
      catalog: { body: PI_CATALOG },
      selection: null,
    });
    await mount();
    await act(async () => {
      value().selectHarness('pi');
    });
    await flush();

    expect(value().modelId).toBe('');
    expect(value().persistedSelection).toBeNull();
    expect(value().canSend).toBe(false);
    // Nothing was persisted — no PUT fired for an unset selection.
    const putCalls = fetchMock.mock.calls.filter(
      (c) => String((c[1] as RequestInit)?.method).toUpperCase() === 'PUT',
    );
    expect(putCalls).toHaveLength(0);
  });

  it('persists the structured tuple and only enables send AFTER the PUT resolves (no race ahead of persistence)', async () => {
    installFetch({
      harnesses: [{ id: 'pi', displayName: 'Pi', capabilities: [] }],
      catalog: { body: PI_CATALOG },
      selection: null,
      deferPut: true,
    });
    await mount();
    await act(async () => {
      value().selectHarness('pi');
    });
    await flush();
    await act(async () => {
      value().selectProvider('openai');
    });
    await act(async () => {
      value().selectModel('openai', 'gpt-5');
    });
    await flush();

    // PUT is in flight (deferred) — send MUST NOT be enabled yet.
    expect(value().canSend).toBe(false);

    await act(async () => {
      putDeferred?.resolve(
        json({ selection: { harnessId: 'pi', providerId: 'openai', modelId: 'gpt-5' } }),
      );
    });
    await flush();

    expect(putBodies).toContainEqual({ harnessId: 'pi', providerId: 'openai', modelId: 'gpt-5' });
    expect(value().persistedSelection).toEqual({
      harnessId: 'pi',
      providerId: 'openai',
      modelId: 'gpt-5',
    });
    expect(value().canSend).toBe(true);
    // Task Five: the composer sends the nested `persistedSelection` tuple directly.
    // The Task-Four compat flat `projection` ({provider, modelId}) is removed — the
    // harnessId must never be dropped on the way to the wire.
    expect('projection' in value()).toBe(false);
  });

  it('keeps a stale/unavailable persisted selection visibly displayed rather than silently dropping it', async () => {
    installFetch({
      harnesses: [{ id: 'pi', displayName: 'Pi', capabilities: [] }],
      catalog: { body: PI_CATALOG },
      selection: { harnessId: 'pi', providerId: 'openai', modelId: 'retired-model' },
    });
    await mount();

    // The persisted tuple is displayed even though its model is gone from the catalog.
    expect(value().persistedSelection).toEqual({
      harnessId: 'pi',
      providerId: 'openai',
      modelId: 'retired-model',
    });
    expect(value().modelId).toBe('retired-model');
    expect(value().isStale).toBe(true);
    // A stale model is not a valid catalog option, so send stays disabled.
    expect(value().canSend).toBe(false);
  });

  it('disables send for an empty catalog (no viable model) and never fabricates one', async () => {
    installFetch({
      harnesses: [{ id: 'pi', displayName: 'Pi', capabilities: [] }],
      catalog: { body: { harnessId: 'pi', version: '1', fingerprint: 'f', models: [] } },
      selection: null,
    });
    await mount();
    await act(async () => {
      value().selectHarness('pi');
    });
    await flush();

    expect(value().catalog?.models ?? []).toHaveLength(0);
    expect(value().canSend).toBe(false);
  });

  it('marks the catalog unavailable and disables send when the catalog request 404s', async () => {
    installFetch({
      harnesses: [{ id: 'pi', displayName: 'Pi', capabilities: [] }],
      catalog: {
        body: { code: 'adapter_unavailable', message: 'x', harnessId: 'pi' },
        status: 404,
      },
      selection: null,
    });
    await mount();
    await act(async () => {
      value().selectHarness('pi');
    });
    await flush();

    expect(value().catalogUnavailable).toBe(true);
    expect(value().canSend).toBe(false);
  });

  it('on a 422 persist, keeps the requested tuple visible, surfaces a typed error, and leaves send disabled', async () => {
    installFetch({
      harnesses: [{ id: 'pi', displayName: 'Pi', capabilities: [] }],
      catalog: {
        body: {
          ...PI_CATALOG,
          models: [{ ...PI_CATALOG.models[0], availability: 'unavailable' }],
        },
      },
      selection: null,
    });
    // Override PUT to 422.
    const fetchMock = vi.fn(async (input: unknown, init?: RequestInit) => {
      const url = String(input);
      const method = String(init?.method ?? 'GET').toUpperCase();
      if (url === '/api/harnesses')
        return json([{ id: 'pi', displayName: 'Pi', capabilities: [] }]);
      if (url.endsWith('/catalog')) return json(PI_CATALOG);
      if (url === '/api/chat/preferences/selection' && method === 'GET')
        return json({ selection: null });
      if (url === '/api/chat/preferences/selection' && method === 'PUT') {
        return json(
          {
            code: 'model_unavailable',
            message: 'nope',
            selection: { harnessId: 'a', providerId: 'b', modelId: 'c' },
          },
          422,
        );
      }
      return new Response('nf', { status: 404 });
    });
    vi.stubGlobal('fetch', fetchMock);

    await mount();
    await act(async () => {
      value().selectHarness('pi');
    });
    await flush();
    await act(async () => {
      value().selectProvider('openai');
    });
    await act(async () => {
      value().selectModel('openai', 'gpt-5');
    });
    await flush();

    expect(value().modelId).toBe('gpt-5');
    expect(value().persistError?.code).toBe('model_unavailable');
    expect(value().persistError?.requested).toEqual({
      harnessId: 'pi',
      providerId: 'openai',
      modelId: 'gpt-5',
    });
    expect(value().persistedSelection).toBeNull();
    expect(value().canSend).toBe(false);
  });

  it('invalidates the model on a provider change and keeps send disabled until the new tuple persists', async () => {
    installFetch({
      harnesses: [{ id: 'pi', displayName: 'Pi', capabilities: [] }],
      catalog: { body: PI_CATALOG },
      selection: null,
    });
    await mount();
    await act(async () => {
      value().selectHarness('pi');
    });
    await flush();
    await act(async () => {
      value().selectProvider('openai');
    });
    await act(async () => {
      value().selectModel('openai', 'gpt-5');
    });
    await flush();
    // A valid provider-A tuple has persisted.
    expect(value().canSend).toBe(true);
    expect(value().persistedSelection).toEqual({
      harnessId: 'pi',
      providerId: 'openai',
      modelId: 'gpt-5',
    });

    // Switching provider clears the model that no longer belongs to it.
    await act(async () => {
      value().selectProvider('anthropic');
    });
    expect(value().modelId).toBe('');
    expect(value().canSend).toBe(false);

    // Send stays disabled until the new exact provider-B tuple persists.
    await act(async () => {
      value().selectModel('anthropic', 'claude');
    });
    await flush();
    expect(value().canSend).toBe(true);
    expect(value().persistedSelection).toEqual({
      harnessId: 'pi',
      providerId: 'anthropic',
      modelId: 'claude',
    });
    // Task Five: no compat flat projection — the nested persistedSelection is the wire tuple.
    expect('projection' in value()).toBe(false);
  });

  it('does not enable send on a model pick until the PUT for that exact new tuple resolves', async () => {
    installFetch({
      harnesses: [{ id: 'pi', displayName: 'Pi', capabilities: [] }],
      catalog: { body: PI_CATALOG },
      selection: { harnessId: 'pi', providerId: 'openai', modelId: 'gpt-5' },
      deferPut: true,
    });
    await mount();
    // The persisted, in-catalog tuple is sendable after mount (no PUT needed).
    expect(value().canSend).toBe(true);

    await act(async () => {
      value().selectProvider('anthropic');
    });
    expect(value().modelId).toBe('');
    expect(value().canSend).toBe(false);

    await act(async () => {
      value().selectModel('anthropic', 'claude');
    });
    await flush();
    // PUT for the new tuple is still in flight — send MUST stay disabled.
    expect(value().canSend).toBe(false);

    await act(async () => {
      putDeferred?.resolve(
        json({ selection: { harnessId: 'pi', providerId: 'anthropic', modelId: 'claude' } }),
      );
    });
    await flush();
    expect(value().canSend).toBe(true);
    // Task Five: no compat flat projection — the nested persistedSelection is the wire tuple.
    expect('projection' in value()).toBe(false);
  });

  it('never requests any /api/providers* endpoint across the whole flow', async () => {
    const fetchMock = installFetch({
      harnesses: [{ id: 'pi', displayName: 'Pi', capabilities: [] }],
      catalog: { body: PI_CATALOG },
      selection: { harnessId: 'pi', providerId: 'openai', modelId: 'gpt-5' },
    });
    await mount();
    await act(async () => {
      value().selectProvider('anthropic');
    });
    await act(async () => {
      value().selectModel('anthropic', 'claude');
    });
    await flush();

    for (const call of fetchMock.mock.calls) {
      expect(String(call[0])).not.toContain('/api/providers');
    }
  });
});
