import React from 'react';
import '@testing-library/jest-dom';
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { ToastProvider } from './toast';
import { I18nProvider } from './i18n/I18nProvider';
import { api } from './api';
import { JobApplication } from './types';

jest.mock('./api', () => ({
  api: {
    get: jest.fn(),
    patch: jest.fn(() => Promise.resolve({ data: {} })),
  },
}));

// eslint-disable-next-line import/first
import KanbanBoard from './components/KanbanBoard';

const mockedApi = api as jest.Mocked<typeof api>;

function job(id: number, jobTitle: string, status: string, overrides: Partial<JobApplication> = {}): JobApplication {
  return {
    id,
    jobTitle,
    company: { id: 1, name: 'Acme' },
    companyId: 1,
    status,
    // Pre-application stages carry no applied date — that is the whole point of the split.
    dateApplied: null,
    savedAt: new Date('2026-07-01T00:00:00Z').toISOString(),
    daysSince: null,
    ...overrides,
  } as JobApplication;
}

function renderBoard() {
  return render(
    <ToastProvider>
      <I18nProvider>
        <KanbanBoard />
      </I18nProvider>
    </ToastProvider>,
  );
}

beforeEach(() => {
  jest.clearAllMocks();
});

test('the board collapses ten stages into the three agreed groups', async () => {
  mockedApi.get.mockResolvedValue({
    data: [
      job(1, 'Saved Role', 'Saved'),
      job(2, 'Preparing Role', 'Preparing'),
      job(3, 'Applied Role', 'Applied', { dateApplied: new Date('2026-07-02T00:00:00Z').toISOString(), daysSince: 15 }),
      job(4, 'Offer Role', 'Offer', { dateApplied: new Date('2026-07-02T00:00:00Z').toISOString(), daysSince: 15 }),
      job(5, 'Withdrawn Role', 'Withdrawn', { dateApplied: new Date('2026-07-02T00:00:00Z').toISOString(), daysSince: 15 }),
    ],
  } as any);

  renderBoard();

  await screen.findByText('Not Applied');
  expect(screen.getByText('Active')).toBeInTheDocument();
  expect(screen.getByText('Closed')).toBeInTheDocument();

  // Only the three groups — no "Other" column, because no custom statuses are present.
  expect(screen.queryByText('Other')).not.toBeInTheDocument();

  // Every job is placed, and grouping does not hide which stage it is actually in.
  expect(screen.getByText('Saved Role')).toBeInTheDocument();
  expect(screen.getByText('Withdrawn Role')).toBeInTheDocument();
  expect(screen.getByText('Withdrawn')).toBeInTheDocument();
  expect(screen.getByText('Preparing')).toBeInTheDocument();
  // Offer groups under Active even though it is a Success category on the backend.
  expect(screen.getByText('Offer')).toBeInTheDocument();
});

test('a custom status gets an Other column that is not a drop target', async () => {
  mockedApi.get.mockResolvedValue({
    data: [job(1, 'Odd Role', 'Take-home assignment')],
  } as any);

  renderBoard();

  await screen.findByText('Other');
  // Custom statuses survive rather than being coerced into a canonical stage.
  expect(screen.getByText('Take-home assignment')).toBeInTheDocument();
});

test('dropping onto a group applies that group entry stage', async () => {
  mockedApi.get.mockResolvedValue({ data: [job(7, 'Saved Role', 'Saved')] } as any);

  renderBoard();

  const card = await screen.findByText('Saved Role');
  const column = screen.getByText('Active').closest('div')!.parentElement!.parentElement!;

  fireEvent.dragStart(card.closest('.MuiCard-root')!);
  fireEvent.dragOver(column);
  fireEvent.drop(column);

  // A group is not a status, so a coarse drag applies the group's entry stage.
  await waitFor(() => expect(mockedApi.patch).toHaveBeenCalledWith('/jobapplications/7/status', { status: 'Applied' }));
});

test('the card menu offers precise stages grouped by section', async () => {
  mockedApi.get.mockResolvedValue({ data: [job(9, 'Saved Role', 'Saved')] } as any);

  renderBoard();

  await screen.findByText('Saved Role');
  fireEvent.click(screen.getByRole('button', { name: '' }) ?? screen.getAllByRole('button')[0]);

  const menu = await screen.findByRole('menu');
  // Drag is coarse; the menu is where Ghosted and Withdrawn are reachable at all.
  expect(within(menu).getByText(/Ghosted/)).toBeInTheDocument();
  expect(within(menu).getByText(/Withdrawn/)).toBeInTheDocument();
  // The job's own current stage is not offered as a target.
  expect(within(menu).queryByText(/Set status: Saved/)).not.toBeInTheDocument();
});
