import React from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';

// Mock the toast import the component depends on — keeps the test free
// of side-effect side-channels (toast() schedules timers we don't want).
vi.mock('react-hot-toast', () => ({
  default: { error: vi.fn(), success: vi.fn() },
  toast: { error: vi.fn(), success: vi.fn() },
}));

import EngineCompatibilityMatrix from '../components/EngineCompatibilityMatrix';

/** Build a minimal AllEnginesResponse with the three rows the plan calls for. */
function makeEnginesResponse({
  inProcessAvailable = true,
  inProcessHasLastError = false,
} = {}) {
  return {
    tts: {
      active: 'omnivoice',
      backends: [
        {
          id: 'omnivoice',
          display_name: 'OmniVoice (test)',
          available: inProcessAvailable,
          reason: inProcessAvailable ? null : 'omnivoice package missing',
          install_hint: 'pip install omnivoice',
          last_error: inProcessHasLastError ? 'previous load failed' : null,
          isolation_mode: 'in-process',
          gpu_compat: ['cuda', 'mps', 'cpu'],
        },
        {
          id: 'kittentts',
          display_name: 'KittenTTS (test)',
          available: false,
          reason: 'kittentts not installed',
          install_hint: 'pip install kittentts',
          last_error: 'auth failed for hf_***REDACTED***',
          isolation_mode: 'in-process',
          gpu_compat: ['cpu'],
        },
        {
          id: 'indextts2',
          display_name: 'IndexTTS2 (test)',
          available: true,
          reason: null,
          install_hint: 'git clone …',
          last_error: null,
          isolation_mode: 'subprocess',
          gpu_compat: ['cuda', 'mps', 'cpu'],
        },
      ],
    },
    asr: { active: 'whisperx', backends: [] },
    llm: { active: 'off', backends: [] },
  };
}

describe('EngineCompatibilityMatrix', () => {
  beforeEach(() => {
    vi.useRealTimers();
  });

  it('renders one row per backend with the documented columns', async () => {
    const apiListEngines = vi.fn().mockResolvedValue(makeEnginesResponse());
    render(
      <EngineCompatibilityMatrix
        family="tts"
        apiListEngines={apiListEngines}
        apiGetEngineHealth={vi.fn()}
      />,
    );

    await waitFor(() => {
      expect(screen.getByText('OmniVoice (test)')).toBeInTheDocument();
    });
    expect(apiListEngines).toHaveBeenCalledTimes(1);

    // Three engine rows, one per registered backend.
    expect(screen.getAllByRole('row').length).toBe(3);
    expect(screen.getByText('KittenTTS (test)')).toBeInTheDocument();
    expect(screen.getByText('IndexTTS2 (test)')).toBeInTheDocument();
  });

  it('shows isolation_mode badge per row (subprocess for IndexTTS, in-process for the others)', async () => {
    const apiListEngines = vi.fn().mockResolvedValue(makeEnginesResponse());
    render(
      <EngineCompatibilityMatrix
        family="tts"
        apiListEngines={apiListEngines}
        apiGetEngineHealth={vi.fn()}
      />,
    );

    await waitFor(() => screen.getByText('IndexTTS2 (test)'));

    const indexRow = screen.getByText('IndexTTS2 (test)').closest('[role="row"]');
    const omniRow = screen.getByText('OmniVoice (test)').closest('[role="row"]');
    const kittenRow = screen.getByText('KittenTTS (test)').closest('[role="row"]');

    expect(within(indexRow).getByText('subprocess')).toBeInTheDocument();
    expect(within(omniRow).getByText('in-process')).toBeInTheDocument();
    expect(within(kittenRow).getByText('in-process')).toBeInTheDocument();
  });

  it('renders GPU compat chips for each backend', async () => {
    const apiListEngines = vi.fn().mockResolvedValue(makeEnginesResponse());
    render(
      <EngineCompatibilityMatrix
        family="tts"
        apiListEngines={apiListEngines}
        apiGetEngineHealth={vi.fn()}
      />,
    );

    await waitFor(() => screen.getByText('OmniVoice (test)'));

    const omniRow = screen.getByText('OmniVoice (test)').closest('[role="row"]');
    expect(within(omniRow).getByText('CUDA')).toBeInTheDocument();
    expect(within(omniRow).getByText('MPS')).toBeInTheDocument();
    expect(within(omniRow).getByText('CPU')).toBeInTheDocument();

    const kittenRow = screen.getByText('KittenTTS (test)').closest('[role="row"]');
    // KittenTTS is CPU-only.
    expect(within(kittenRow).getByText('CPU')).toBeInTheDocument();
    expect(within(kittenRow).queryByText('CUDA')).not.toBeInTheDocument();
  });

  it('shows the install reason inline when a backend is unavailable', async () => {
    const apiListEngines = vi.fn().mockResolvedValue(makeEnginesResponse());
    render(
      <EngineCompatibilityMatrix
        family="tts"
        apiListEngines={apiListEngines}
        apiGetEngineHealth={vi.fn()}
      />,
    );

    await waitFor(() => screen.getByText('KittenTTS (test)'));
    const kittenRow = screen.getByText('KittenTTS (test)').closest('[role="row"]');
    expect(within(kittenRow).getByText('kittentts not installed')).toBeInTheDocument();
    expect(within(kittenRow).getByText(/Unavailable/i)).toBeInTheDocument();
  });

  it('renders a "Last error" line when last_error is populated', async () => {
    const apiListEngines = vi.fn().mockResolvedValue(makeEnginesResponse());
    render(
      <EngineCompatibilityMatrix
        family="tts"
        apiListEngines={apiListEngines}
        apiGetEngineHealth={vi.fn()}
      />,
    );

    await waitFor(() => screen.getByText('KittenTTS (test)'));
    const lastErrEls = screen.getAllByTestId('last-error');
    expect(lastErrEls.length).toBeGreaterThan(0);
    // The masked sentinel survives the redactor — confirms the row renders
    // the cache verbatim and does NOT try to "clean up" the masked string.
    expect(lastErrEls[0].textContent).toMatch(/hf_\*\*\*REDACTED\*\*\*/);
  });

  it('clicking Test engine fires getEngineHealth and renders latency_ms', async () => {
    const apiListEngines = vi.fn().mockResolvedValue(makeEnginesResponse());
    const apiGetEngineHealth = vi.fn().mockResolvedValue({
      id: 'indextts2', ok: true, message: 'pong', latency_ms: 1234,
    });
    render(
      <EngineCompatibilityMatrix
        family="tts"
        apiListEngines={apiListEngines}
        apiGetEngineHealth={apiGetEngineHealth}
      />,
    );

    await waitFor(() => screen.getByText('IndexTTS2 (test)'));
    const indexRow = screen.getByText('IndexTTS2 (test)').closest('[role="row"]');
    const testBtn = within(indexRow).getByRole('button', { name: /test indextts2/i });
    fireEvent.click(testBtn);

    await waitFor(() => {
      expect(apiGetEngineHealth).toHaveBeenCalledWith('indextts2');
    });
    await waitFor(() => {
      expect(within(indexRow).getByTestId('health-result-indextts2')).toBeInTheDocument();
    });
    expect(within(indexRow).getByText(/1234 ms/)).toBeInTheDocument();
  });

  it('Test button is disabled while an inflight health request is pending', async () => {
    const apiListEngines = vi.fn().mockResolvedValue(makeEnginesResponse());
    // A health request that never resolves so we can observe the inflight state.
    let resolveHealth;
    const apiGetEngineHealth = vi.fn(() => new Promise((resolve) => { resolveHealth = resolve; }));
    render(
      <EngineCompatibilityMatrix
        family="tts"
        apiListEngines={apiListEngines}
        apiGetEngineHealth={apiGetEngineHealth}
      />,
    );

    await waitFor(() => screen.getByText('IndexTTS2 (test)'));
    const indexRow = screen.getByText('IndexTTS2 (test)').closest('[role="row"]');
    const testBtn = within(indexRow).getByRole('button', { name: /test indextts2/i });
    fireEvent.click(testBtn);

    await waitFor(() => {
      expect(testBtn).toBeDisabled();
    });
    // Second click while inflight must be a no-op — the spy has been called
    // exactly once.
    fireEvent.click(testBtn);
    expect(apiGetEngineHealth).toHaveBeenCalledTimes(1);

    // Release the promise so the test doesn't leak a pending microtask.
    resolveHealth({ id: 'indextts2', ok: true, message: 'pong', latency_ms: 50 });
  });

  it('renders a failure marker when the health route returns ok=false', async () => {
    const apiListEngines = vi.fn().mockResolvedValue(makeEnginesResponse());
    const apiGetEngineHealth = vi.fn().mockResolvedValue({
      id: 'indextts2', ok: false, message: 'spawn failed', latency_ms: 12,
    });
    render(
      <EngineCompatibilityMatrix
        family="tts"
        apiListEngines={apiListEngines}
        apiGetEngineHealth={apiGetEngineHealth}
      />,
    );

    await waitFor(() => screen.getByText('IndexTTS2 (test)'));
    const indexRow = screen.getByText('IndexTTS2 (test)').closest('[role="row"]');
    fireEvent.click(within(indexRow).getByRole('button', { name: /test indextts2/i }));

    await waitFor(() => {
      expect(within(indexRow).getByText(/failed/i)).toBeInTheDocument();
    });
  });
});
