import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { LocationsModal } from '../../components/LocationsModal';
import { api, ApiError } from '../../api/client';

const mockShowToast = vi.fn();
const mockOnClose = vi.fn();
const mockOnPickLocation = vi.fn();

vi.mock('../../api/client', () => ({
  api: {
    getLocations: vi.fn(),
    createLocation: vi.fn(),
    updateLocation: vi.fn(),
    deleteLocation: vi.fn(),
  },
  ApiError: class ApiError extends Error {
    status: number;
    constructor(message: string, status: number) {
      super(message);
      this.status = status;
    }
  },
}));

vi.mock('../../contexts/ToastContext', () => ({
  useToast: () => ({ showToast: mockShowToast }),
}));

const locations = [
  { id: 1, name: 'Shelf A', identifier: null, spool_count: 2, created_at: '2026-01-01', updated_at: '2026-01-01' },
  { id: 2, name: 'Drawer 1', identifier: null, spool_count: 0, created_at: '2026-01-01', updated_at: '2026-01-01' },
];

function renderModal(open = true) {
  const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
  return render(
    <QueryClientProvider client={client}>
      <MemoryRouter>
        <LocationsModal open={open} onClose={mockOnClose} onPickLocation={mockOnPickLocation} />
      </MemoryRouter>
    </QueryClientProvider>,
  );
}

describe('LocationsModal', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.mocked(api.getLocations).mockResolvedValue(locations);
  });

  it('renders nothing when open=false', () => {
    const { container } = renderModal(false);
    expect(container.firstChild).toBeNull();
    expect(api.getLocations).not.toHaveBeenCalled();
  });

  it('renders locations from API when open', async () => {
    renderModal();
    expect(await screen.findByText('Shelf A')).toBeInTheDocument();
    expect(screen.getByText('Drawer 1')).toBeInTheDocument();
    expect(screen.getByText('2')).toBeInTheDocument();
  });

  it('renders empty state when API returns no locations', async () => {
    vi.mocked(api.getLocations).mockResolvedValue([]);
    renderModal();
    expect(await screen.findByText(/locations\.empty|no storage locations/i)).toBeInTheDocument();
  });

  it('opens create editor and calls createLocation on submit', async () => {
    vi.mocked(api.createLocation).mockResolvedValue({
      id: 3,
      name: 'Garage',
      identifier: null,
      spool_count: 0,
      created_at: '2026-01-01',
      updated_at: '2026-01-01',
    });
    const user = userEvent.setup();
    renderModal();
    await screen.findByText('Shelf A');
    await user.click(screen.getByRole('button', { name: /add location|locations\.add/i }));
    const input = screen.getByLabelText(/name|locations\.name/i);
    await user.type(input, 'Garage');
    await user.click(screen.getByRole('button', { name: /save|common\.save/i }));
    await waitFor(() => {
      expect(api.createLocation).toHaveBeenCalledWith({ name: 'Garage' });
    });
    expect(mockShowToast).toHaveBeenCalledWith(expect.stringMatching(/created|locations\.created/i), 'success');
  });

  it('submits create form on Enter key', async () => {
    vi.mocked(api.createLocation).mockResolvedValue({
      id: 3,
      name: 'Garage',
      identifier: null,
      spool_count: 0,
      created_at: '2026-01-01',
      updated_at: '2026-01-01',
    });
    const user = userEvent.setup();
    renderModal();
    await screen.findByText('Shelf A');
    await user.click(screen.getByRole('button', { name: /add location|locations\.add/i }));
    const input = screen.getByLabelText(/name|locations\.name/i);
    await user.type(input, 'Garage{Enter}');
    await waitFor(() => {
      expect(api.createLocation).toHaveBeenCalledWith({ name: 'Garage' });
    });
  });

  it('Escape closes the inner editor first, then the outer modal', async () => {
    const user = userEvent.setup();
    renderModal();
    await screen.findByText('Shelf A');
    // Open the inner editor; both dialogs are now in the DOM.
    await user.click(screen.getByRole('button', { name: /add location|locations\.add/i }));
    expect(screen.getAllByRole('dialog')).toHaveLength(2);
    // First Escape closes the editor only.
    await user.keyboard('{Escape}');
    await waitFor(() => {
      expect(screen.getAllByRole('dialog')).toHaveLength(1);
    });
    expect(mockOnClose).not.toHaveBeenCalled();
    // Second Escape closes the outer modal.
    await user.keyboard('{Escape}');
    await waitFor(() => {
      expect(mockOnClose).toHaveBeenCalledTimes(1);
    });
  });

  it('edits a location and calls updateLocation', async () => {
    vi.mocked(api.updateLocation).mockResolvedValue({
      id: 2,
      name: 'Drawer 2',
      identifier: null,
      spool_count: 0,
      created_at: '2026-01-01',
      updated_at: '2026-01-01',
    });
    const user = userEvent.setup();
    renderModal();
    await screen.findByText('Drawer 1');
    const editButtons = screen.getAllByTitle(/edit|common\.edit/i);
    await user.click(editButtons[1]);
    const input = screen.getByLabelText(/name|locations\.name/i);
    await user.clear(input);
    await user.type(input, 'Drawer 2');
    await user.click(screen.getByRole('button', { name: /save|common\.save/i }));
    await waitFor(() => {
      expect(api.updateLocation).toHaveBeenCalledWith(2, { name: 'Drawer 2' });
    });
    expect(mockShowToast).toHaveBeenCalledWith(expect.stringMatching(/updated|locations\.updated/i), 'success');
  });

  it('deletes an empty location after confirmation', async () => {
    vi.mocked(api.deleteLocation).mockResolvedValue({ status: 'deleted' });
    const user = userEvent.setup();
    renderModal();
    await screen.findByText('Drawer 1');
    const row = screen.getByText('Drawer 1').closest('tr');
    expect(row).not.toBeNull();
    await user.click(within(row!).getByTitle(/^Delete$/i));
    await user.click(screen.getAllByRole('button', { name: /^Delete$/i }).pop()!);
    await waitFor(() => {
      expect(api.deleteLocation).toHaveBeenCalledWith(2);
    });
    expect(mockShowToast).toHaveBeenCalledWith(expect.stringMatching(/deleted|locations\.deleted/i), 'success');
  });

  it('blocks delete when spool_count > 0', async () => {
    renderModal();
    await screen.findByText('Shelf A');
    const blockedDelete = screen.getByTitle(/Remove all spools from this location before deleting/i);
    expect(blockedDelete).toBeDisabled();
  });

  it('shows error toast when create returns 409 duplicate name', async () => {
    vi.mocked(api.createLocation).mockRejectedValue(
      new ApiError('A location with this name already exists', 409),
    );
    const user = userEvent.setup();
    renderModal();
    await screen.findByText('Shelf A');
    await user.click(screen.getByRole('button', { name: /add location|locations\.add/i }));
    await user.type(screen.getByLabelText(/name|locations\.name/i), 'Shelf A');
    await user.click(screen.getByRole('button', { name: /save|common\.save/i }));
    await waitFor(() => {
      expect(mockShowToast).toHaveBeenCalledWith('A location with this name already exists', 'error');
    });
  });

  it('shows error toast when delete fails', async () => {
    vi.mocked(api.deleteLocation).mockRejectedValue(new Error('Delete failed'));
    const user = userEvent.setup();
    renderModal();
    await screen.findByText('Drawer 1');
    const row = screen.getByText('Drawer 1').closest('tr');
    expect(row).not.toBeNull();
    await user.click(within(row!).getByTitle(/^Delete$/i));
    await user.click(screen.getAllByRole('button', { name: /^Delete$/i }).pop()!);
    await waitFor(() => {
      expect(mockShowToast).toHaveBeenCalledWith('Delete failed', 'error');
    });
  });

  it('row click calls onPickLocation and onClose', async () => {
    const user = userEvent.setup();
    renderModal();
    await screen.findByText('Shelf A');
    const row = screen.getByText('Shelf A').closest('tr')!;
    await user.click(row);
    expect(mockOnPickLocation).toHaveBeenCalledWith(1);
    expect(mockOnClose).toHaveBeenCalledTimes(1);
  });

  it('shows error toast when rename returns 409 collision', async () => {
    vi.mocked(api.updateLocation).mockRejectedValue(
      new ApiError('A location with this name already exists', 409),
    );
    const user = userEvent.setup();
    renderModal();
    await screen.findByText('Drawer 1');

    const editButtons = screen.getAllByTitle(/edit|common\.edit/i);
    await user.click(editButtons[1]);
    const input = screen.getByLabelText(/name|locations\.name/i);
    await user.clear(input);
    await user.type(input, 'Shelf A');
    await user.click(screen.getByRole('button', { name: /save|common\.save/i }));

    await waitFor(() => {
      expect(mockShowToast).toHaveBeenCalledWith(
        'A location with this name already exists',
        'error',
      );
    });
  });
});
