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

import { MachineShell } from '../../src/components/machine/machine-shell';

const { mockUseQuery, fetchSpy } = vi.hoisted(() => ({
  mockUseQuery: vi.fn(),
  fetchSpy: vi.fn(),
}));

vi.mock('convex/react', () => ({
  useQuery: mockUseQuery,
}));

vi.mock('next/navigation', () => ({
  useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
}));

vi.mock('sonner', () => ({
  toast: { error: vi.fn(), success: vi.fn() },
}));

vi.mock('@/components/agent-workspace/xterm-session', () => ({
  XtermSession: () => <div>terminal</div>,
}));

vi.mock('@/components/agent-workspace/code-editor', () => ({
  CodeEditor: () => <div>editor</div>,
}));

const stoppedStatus = {
  running: false,
  image: 'spoon/devbox:latest',
  startedAt: null,
  memoryLimitBytes: null,
  containerName: 'spoon-home-gib',
};

const runningStatus = {
  ...stoppedStatus,
  running: true,
  startedAt: new Date().toISOString(),
};

// Point the status endpoint at a running box so the Stop/Restart controls render.
const useRunningStatus = () => {
  fetchSpy.mockImplementation((input: RequestInfo | URL) => {
    const url =
      typeof input === 'string'
        ? input
        : input instanceof URL
          ? input.href
          : input.url;
    if (url.startsWith('/api/box/status')) {
      return Promise.resolve(jsonResponse({ status: runningStatus }));
    }
    if (url.startsWith('/api/box/tree')) {
      return Promise.resolve(jsonResponse({ tree }));
    }
    if (url.startsWith('/api/box/lifecycle')) {
      return Promise.resolve(jsonResponse({ status: stoppedStatus }));
    }
    return Promise.resolve(jsonResponse({}));
  });
};

const lifecycleCalls = () =>
  fetchSpy.mock.calls.filter(([input]) => {
    const url = typeof input === 'string' ? input : '';
    return url.startsWith('/api/box/lifecycle');
  });

const tree = {
  name: '',
  path: '',
  type: 'directory',
  children: [{ name: 'README.md', path: 'README.md', type: 'file' }],
};

const jsonResponse = (body: unknown) => ({
  ok: true,
  json: () => Promise.resolve(body),
  text: () => Promise.resolve(JSON.stringify(body)),
});

beforeEach(() => {
  mockUseQuery.mockReturnValue({
    username: 'gib',
    firstName: 'Gib',
    enabled: true,
  });
  fetchSpy.mockImplementation((input: RequestInfo | URL) => {
    const url =
      typeof input === 'string'
        ? input
        : input instanceof URL
          ? input.href
          : input.url;
    if (url.startsWith('/api/box/status')) {
      return Promise.resolve(jsonResponse({ status: stoppedStatus }));
    }
    if (url.startsWith('/api/box/tree')) {
      return Promise.resolve(jsonResponse({ tree }));
    }
    if (url.startsWith('/api/box/lifecycle')) {
      return Promise.resolve(
        jsonResponse({
          status: {
            ...stoppedStatus,
            running: true,
            startedAt: new Date().toISOString(),
          },
        }),
      );
    }
    return Promise.resolve(jsonResponse({}));
  });
  vi.stubGlobal('fetch', fetchSpy);
});

afterEach(() => {
  cleanup();
  vi.unstubAllGlobals();
  vi.clearAllMocks();
});

describe('MachineShell', () => {
  it('renders a stopped status card with a Start button', async () => {
    render(<MachineShell />);
    expect(await screen.findByText('Stopped')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: 'Start' })).toBeInTheDocument();
  });

  it('starts the box when Start is clicked', async () => {
    render(<MachineShell />);
    const startButton = await screen.findByRole('button', { name: 'Start' });
    fireEvent.click(startButton);
    await waitFor(() =>
      expect(fetchSpy).toHaveBeenCalledWith(
        '/api/box/lifecycle',
        expect.objectContaining({
          method: 'POST',
          body: JSON.stringify({ action: 'start' }),
        }),
      ),
    );
  });

  it('renders the home file tree', async () => {
    render(<MachineShell />);
    expect(await screen.findByText('README.md')).toBeInTheDocument();
  });

  it('renders the terminal', async () => {
    render(<MachineShell />);
    expect(await screen.findByText('terminal')).toBeInTheDocument();
  });

  it('does not stop the box until the confirmation is accepted', async () => {
    useRunningStatus();
    render(<MachineShell />);
    const stopButton = await screen.findByRole('button', { name: 'Stop' });
    fireEvent.click(stopButton);

    // The confirmation dialog intervenes: no lifecycle POST fires on the click.
    const confirm = await screen.findByRole('button', { name: 'Stop machine' });
    expect(lifecycleCalls()).toHaveLength(0);

    fireEvent.click(confirm);
    await waitFor(() =>
      expect(fetchSpy).toHaveBeenCalledWith(
        '/api/box/lifecycle',
        expect.objectContaining({
          method: 'POST',
          body: JSON.stringify({ action: 'stop' }),
        }),
      ),
    );
  });

  it('does not restart the box until the confirmation is accepted', async () => {
    useRunningStatus();
    render(<MachineShell />);
    const restartButton = await screen.findByRole('button', {
      name: 'Restart',
    });
    fireEvent.click(restartButton);

    const confirm = await screen.findByRole('button', {
      name: 'Restart machine',
    });
    expect(lifecycleCalls()).toHaveLength(0);

    fireEvent.click(confirm);
    await waitFor(() =>
      expect(fetchSpy).toHaveBeenCalledWith(
        '/api/box/lifecycle',
        expect.objectContaining({
          method: 'POST',
          body: JSON.stringify({ action: 'restart' }),
        }),
      ),
    );
  });
});
