// FE-PLANNER-RESMODAL-001 to FE-PLANNER-RESMODAL-052
import { render, screen, waitFor, fireEvent } from '../../../tests/helpers/render';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
import { server } from '../../../tests/helpers/msw/server';
import { useAuthStore } from '../../store/authStore';
import { useTripStore } from '../../store/tripStore';
import { useAddonStore } from '../../store/addonStore';
import { resetAllStores, seedStore } from '../../../tests/helpers/store';
import {
  buildUser,
  buildTrip,
  buildDay,
  buildPlace,
  buildAssignment,
  buildReservation,
  buildTripFile,
} from '../../../tests/helpers/factories';
import { ReservationModal } from './ReservationModal';

// Mock react-router-dom useParams
vi.mock('react-router-dom', async (importActual) => {
  const actual = await importActual<typeof import('react-router-dom')>();
  return { ...actual, useParams: () => ({ id: '1' }) };
});

// Mock CustomDatePicker as a simple text input
vi.mock('../shared/CustomDateTimePicker', () => ({
  CustomDatePicker: ({ value, onChange, placeholder }: { value: string; onChange: (v: string) => void; placeholder?: string }) => (
    <input
      data-testid="date-picker"
      type="text"
      value={value}
      onChange={e => onChange(e.target.value)}
      placeholder={placeholder ?? 'YYYY-MM-DD'}
    />
  ),
}));

// Mock CustomTimePicker as a simple text input
vi.mock('../shared/CustomTimePicker', () => ({
  default: ({ value, onChange, placeholder }: { value: string; onChange: (v: string) => void; placeholder?: string }) => (
    <input
      data-testid="time-picker"
      type="text"
      value={value}
      onChange={e => onChange(e.target.value)}
      placeholder={placeholder ?? '00:00'}
    />
  ),
}));

const defaultProps = {
  isOpen: true,
  onClose: vi.fn(),
  onSave: vi.fn().mockResolvedValue(undefined),
  reservation: null,
  days: [],
  places: [],
  assignments: {},
  selectedDayId: null,
  files: [],
  onFileUpload: vi.fn().mockResolvedValue(undefined),
  onFileDelete: vi.fn().mockResolvedValue(undefined),
  accommodations: [],
};

beforeEach(() => {
  resetAllStores();
  seedStore(useAuthStore, { user: buildUser(), isAuthenticated: true });
  seedStore(useTripStore, { trip: buildTrip({ id: 1 }), budgetItems: [] });
  // addonStore: budget addon disabled
  vi.clearAllMocks();
});

describe('ReservationModal', () => {
  // ── Rendering ──────────────────────────────────────────────────────────────

  it('FE-PLANNER-RESMODAL-001: renders without crashing', () => {
    render(<ReservationModal {...defaultProps} />);
    expect(document.body).toBeInTheDocument();
  });

  it('FE-PLANNER-RESMODAL-002: shows "New Reservation" title for new reservation', () => {
    render(<ReservationModal {...defaultProps} reservation={null} />);
    expect(screen.getByText(/New Reservation/i)).toBeInTheDocument();
  });

  it('FE-PLANNER-RESMODAL-003: shows "Edit Reservation" title when editing', () => {
    const res = buildReservation({ title: 'Nice Dinner', type: 'restaurant' });
    render(<ReservationModal {...defaultProps} reservation={res} />);
    expect(screen.getByText(/Edit Reservation/i)).toBeInTheDocument();
  });

  it('FE-PLANNER-RESMODAL-004: title input is required — onSave not called with empty title', async () => {
    const onSave = vi.fn().mockResolvedValue(undefined);
    render(<ReservationModal {...defaultProps} onSave={onSave} />);

    const submitBtn = screen.getByRole('button', { name: /^Add$/i });
    await userEvent.click(submitBtn);
    expect(onSave).not.toHaveBeenCalled();
  });

  it('FE-PLANNER-RESMODAL-005: all 5 type buttons are visible (transport types removed)', () => {
    render(<ReservationModal {...defaultProps} />);
    expect(screen.getByRole('button', { name: /Accommodation/i })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /Restaurant/i })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /Event/i })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /Tour/i })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /Other/i })).toBeInTheDocument();
    expect(screen.queryByRole('button', { name: /^Flight$/i })).not.toBeInTheDocument();
    expect(screen.queryByRole('button', { name: /^Train$/i })).not.toBeInTheDocument();
    expect(screen.queryByRole('button', { name: /^Car$/i })).not.toBeInTheDocument();
    expect(screen.queryByRole('button', { name: /^Cruise$/i })).not.toBeInTheDocument();
  });

  // ── Type selection ──────────────────────────────────────────────────────────

  it('FE-PLANNER-RESMODAL-006: clicking Event type button activates it', async () => {
    render(<ReservationModal {...defaultProps} />);
    const eventBtn = screen.getByRole('button', { name: /Event/i });
    await userEvent.click(eventBtn);
    expect(eventBtn).toHaveStyle({ background: 'var(--text-primary)' });
  });

  it('FE-PLANNER-RESMODAL-008: hotel type shows check-in/check-out time fields', async () => {
    render(<ReservationModal {...defaultProps} />);
    await userEvent.click(screen.getByRole('button', { name: /Accommodation/i }));
    const checkInLabels = screen.getAllByText(/Check-in/i);
    expect(checkInLabels.length).toBeGreaterThanOrEqual(1);
    expect(screen.getByText(/Check-out/i)).toBeInTheDocument();
  });

  it('FE-PLANNER-RESMODAL-009: restaurant type shows location field', async () => {
    render(<ReservationModal {...defaultProps} />);
    await userEvent.click(screen.getByRole('button', { name: /Restaurant/i }));
    expect(screen.getByPlaceholderText(/Address, Airport/i)).toBeInTheDocument();
  });

  it('FE-PLANNER-RESMODAL-010: hotel type hides assignment picker', async () => {
    const day = buildDay({ id: 1, title: 'Day 1' });
    const place = buildPlace({ name: 'Museum' });
    const assignment = buildAssignment({ id: 99, day_id: 1, place });
    render(
      <ReservationModal
        {...defaultProps}
        days={[day]}
        assignments={{ '1': [assignment] }}
      />
    );
    // Switch to hotel type
    await userEvent.click(screen.getByRole('button', { name: /Accommodation/i }));
    expect(screen.queryByText(/Link to day assignment/i)).not.toBeInTheDocument();
  });

  // ── Form population from existing reservation ──────────────────────────────

  it('FE-PLANNER-RESMODAL-011: editing pre-fills title', () => {
    const res = buildReservation({ title: 'Paris Hotel', type: 'hotel', status: 'confirmed' });
    render(<ReservationModal {...defaultProps} reservation={res} />);
    expect(screen.getByDisplayValue('Paris Hotel')).toBeInTheDocument();
  });

  it('FE-PLANNER-RESMODAL-012: editing pre-fills confirmation number', () => {
    const res = buildReservation({ confirmation_number: 'XYZ123' });
    render(<ReservationModal {...defaultProps} reservation={res} />);
    expect(screen.getByDisplayValue('XYZ123')).toBeInTheDocument();
  });

  it('FE-PLANNER-RESMODAL-013: editing pre-fills notes', () => {
    const res = buildReservation({ notes: 'Breakfast included' });
    render(<ReservationModal {...defaultProps} reservation={res} />);
    expect(screen.getByDisplayValue('Breakfast included')).toBeInTheDocument();
  });

  it('FE-PLANNER-RESMODAL-014: editing pre-fills type — restaurant type shows location field', () => {
    const res = buildReservation({ type: 'restaurant', location: 'Via Roma 1' });
    render(<ReservationModal {...defaultProps} reservation={res} />);
    expect(screen.getByDisplayValue('Via Roma 1')).toBeInTheDocument();
  });

  // ── Validation ──────────────────────────────────────────────────────────────

  it('FE-PLANNER-RESMODAL-015: end datetime before start shows error and blocks submit', async () => {
    const onSave = vi.fn().mockResolvedValue(undefined);
    const addToast = vi.fn();
    window.__addToast = addToast;

    render(<ReservationModal {...defaultProps} onSave={onSave} />);

    // Fill in the title
    await userEvent.type(screen.getByPlaceholderText(/e\.g\. Lufthansa/i), 'My Flight');

    // Set start date/time via the date-picker inputs (mocked as text inputs)
    // reservation_time is rendered as two separate pickers: date part and time part
    const datePickers = screen.getAllByTestId('date-picker');
    const timePickers = screen.getAllByTestId('time-picker');

    // First date picker = start date, second = end date
    fireEvent.change(datePickers[0], { target: { value: '2025-06-10' } });
    fireEvent.change(timePickers[0], { target: { value: '10:00' } });
    // End date before start date
    fireEvent.change(datePickers[1], { target: { value: '2025-06-09' } });
    fireEvent.change(timePickers[1], { target: { value: '09:00' } });

    // When isEndBeforeStart=true the submit button is disabled, so fire submit on the form directly.
    // The Save button now lives in the Modal's sticky footer (outside the <form>), so we query
    // the form by tag instead of walking up from the button.
    const form = document.querySelector('form')!;
    fireEvent.submit(form);

    expect(onSave).not.toHaveBeenCalled();
    expect(addToast).toHaveBeenCalledWith(
      expect.stringMatching(/End date\/time must be after start/i),
      'error',
      undefined,
    );

    delete window.__addToast;
  });

  // ── Submit flow ─────────────────────────────────────────────────────────────

  it('FE-PLANNER-RESMODAL-016: submitting valid restaurant booking calls onSave with correct shape', async () => {
    const onSave = vi.fn().mockResolvedValue(undefined);
    render(<ReservationModal {...defaultProps} onSave={onSave} />);

    await userEvent.click(screen.getByRole('button', { name: /Restaurant/i }));
    await userEvent.type(screen.getByPlaceholderText(/e\.g\. Lufthansa/i), 'Le Jules Verne');

    await userEvent.click(screen.getByRole('button', { name: /^Add$/i }));

    await waitFor(() => expect(onSave).toHaveBeenCalled());
    expect(onSave).toHaveBeenCalledWith(
      expect.objectContaining({ title: 'Le Jules Verne', type: 'restaurant' })
    );
  });

  it('FE-PLANNER-RESMODAL-017: status confirmed — onSave called with status confirmed', async () => {
    const onSave = vi.fn().mockResolvedValue(undefined);
    render(<ReservationModal {...defaultProps} onSave={onSave} />);

    await userEvent.type(screen.getByPlaceholderText(/e\.g\. Lufthansa/i), 'Test Booking');

    // The status CustomSelect renders as a button for its trigger — check for "Pending" text and change it
    // CustomSelect renders a div/button with the current value label. We look for the status select area.
    // Since CustomSelect is not mocked, we find the select by its displayed value.
    // The easiest approach: render with a reservation that has status 'confirmed'
    const res = buildReservation({ status: 'confirmed', type: 'flight', title: 'My Booking' });
    const { unmount } = render(<ReservationModal {...defaultProps} reservation={res} onSave={onSave} />);
    const updateBtn = screen.getAllByRole('button', { name: /Update/i })[0];
    await userEvent.click(updateBtn);

    await waitFor(() => expect(onSave).toHaveBeenCalled());
    expect(onSave).toHaveBeenCalledWith(
      expect.objectContaining({ status: 'confirmed' })
    );
    unmount();
  });

  it('FE-PLANNER-RESMODAL-018: onClose NOT called after successful save (parent controls closing)', async () => {
    const onClose = vi.fn();
    const onSave = vi.fn().mockResolvedValue(undefined);
    render(<ReservationModal {...defaultProps} onClose={onClose} onSave={onSave} />);

    await userEvent.type(screen.getByPlaceholderText(/e\.g\. Lufthansa/i), 'Test Booking');
    await userEvent.click(screen.getByRole('button', { name: /^Add$/i }));

    await waitFor(() => expect(onSave).toHaveBeenCalled());
    // The component does NOT call onClose after save — the parent controls that
    expect(onClose).not.toHaveBeenCalled();
  });

  it('FE-PLANNER-RESMODAL-019: save button is disabled while saving', async () => {
    let resolveOnSave: () => void;
    const onSave = vi.fn().mockReturnValue(
      new Promise<void>(resolve => { resolveOnSave = resolve; })
    );
    render(<ReservationModal {...defaultProps} onSave={onSave} />);

    await userEvent.type(screen.getByPlaceholderText(/e\.g\. Lufthansa/i), 'Test Booking');

    const submitBtn = screen.getByRole('button', { name: /^Add$/i });
    await userEvent.click(submitBtn);

    // While promise is pending, the button should be disabled
    await waitFor(() => {
      expect(screen.getByRole('button', { name: /Saving/i })).toBeDisabled();
    });

    // Cleanup
    resolveOnSave!();
  });

  // ── Assignment linking ──────────────────────────────────────────────────────

  it('FE-PLANNER-RESMODAL-020: assignment picker appears when days/assignments are populated (non-hotel)', () => {
    const day = buildDay({ id: 1, title: 'Day 1' });
    const place = buildPlace({ name: 'Museum' });
    const assignment = buildAssignment({ id: 99, day_id: 1, order_index: 0, place });

    render(
      <ReservationModal
        {...defaultProps}
        days={[day]}
        assignments={{ '1': [assignment] }}
      />
    );

    expect(screen.getByText(/Link to day assignment/i)).toBeInTheDocument();
  });

  // ── Files ──────────────────────────────────────────────────────────────────

  it('FE-PLANNER-RESMODAL-022: attached files shown for existing reservation', () => {
    const res = buildReservation({ id: 5 });
    const file = buildTripFile({
      id: 1,
      trip_id: 1,
      original_name: 'ticket.pdf',
    });
    // Add reservation_id field manually (not in standard TripFile type but used in component)
    (file as any).reservation_id = 5;

    render(
      <ReservationModal
        {...defaultProps}
        reservation={res}
        files={[file]}
      />
    );

    expect(screen.getByText('ticket.pdf')).toBeInTheDocument();
  });

  it('FE-PLANNER-RESMODAL-023: Cancel button calls onClose', async () => {
    const onClose = vi.fn();
    render(<ReservationModal {...defaultProps} onClose={onClose} />);

    await userEvent.click(screen.getByRole('button', { name: /Cancel/i }));
    expect(onClose).toHaveBeenCalled();
  });

  // ── Budget addon ─────────────────────────────────────────────────────────────

  it('FE-PLANNER-RESMODAL-024: budget section visible when budget addon is enabled', () => {
    seedStore(useAddonStore, {
      addons: [{ id: 'budget', name: 'Budget', type: 'budget', icon: '', enabled: true }],
      loaded: true,
    });
    render(<ReservationModal {...defaultProps} />);
    expect(screen.getByText(/^Price$/i)).toBeInTheDocument();
    expect(screen.getByText(/Budget category/i)).toBeInTheDocument();
  });

  it('FE-PLANNER-RESMODAL-025: budget price input accepts valid decimal', async () => {
    seedStore(useAddonStore, {
      addons: [{ id: 'budget', name: 'Budget', type: 'budget', icon: '', enabled: true }],
      loaded: true,
    });
    render(<ReservationModal {...defaultProps} />);
    const priceInput = screen.getByPlaceholderText('0.00');
    await userEvent.type(priceInput, '99.99');
    expect((priceInput as HTMLInputElement).value).toBe('99.99');
  });

  it('FE-PLANNER-RESMODAL-026: budget hint shown when price > 0', async () => {
    seedStore(useAddonStore, {
      addons: [{ id: 'budget', name: 'Budget', type: 'budget', icon: '', enabled: true }],
      loaded: true,
    });
    render(<ReservationModal {...defaultProps} />);
    const priceInput = screen.getByPlaceholderText('0.00');
    await userEvent.type(priceInput, '50');
    expect(screen.getByText(/budget entry will be created/i)).toBeInTheDocument();
  });

  it('FE-PLANNER-RESMODAL-027: budget fields included in onSave when price is set', async () => {
    seedStore(useAddonStore, {
      addons: [{ id: 'budget', name: 'Budget', type: 'budget', icon: '', enabled: true }],
      loaded: true,
    });
    const onSave = vi.fn().mockResolvedValue(undefined);
    render(<ReservationModal {...defaultProps} onSave={onSave} />);

    await userEvent.type(screen.getByPlaceholderText(/e\.g\. Lufthansa/i), 'Hotel Paris');
    await userEvent.type(screen.getByPlaceholderText('0.00'), '120');
    await userEvent.click(screen.getByRole('button', { name: /^Add$/i }));

    await waitFor(() => expect(onSave).toHaveBeenCalled());
    expect(onSave).toHaveBeenCalledWith(
      expect.objectContaining({ create_budget_entry: expect.objectContaining({ total_price: 120 }) })
    );
  });

  // ── File upload ───────────────────────────────────────────────────────────────

  it('FE-PLANNER-RESMODAL-028: pending file added for new reservation on file input change', async () => {
    render(<ReservationModal {...defaultProps} reservation={null} />);

    const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
    const testFile = new File(['content'], 'document.pdf', { type: 'application/pdf' });

    fireEvent.change(fileInput, { target: { files: [testFile] } });

    // Pending file name should appear in the list
    await waitFor(() => {
      expect(screen.getByText('document.pdf')).toBeInTheDocument();
    });
  });

  it('FE-PLANNER-RESMODAL-029: attach file button is rendered when onFileUpload provided', () => {
    render(<ReservationModal {...defaultProps} />);
    expect(screen.getByRole('button', { name: /Attach file/i })).toBeInTheDocument();
  });

  it('FE-PLANNER-RESMODAL-030: hotel type — saving calls onSave with correct hotel shape', async () => {
    const onSave = vi.fn().mockResolvedValue(undefined);
    render(<ReservationModal {...defaultProps} onSave={onSave} />);

    await userEvent.click(screen.getByRole('button', { name: /Accommodation/i }));
    await userEvent.type(screen.getByPlaceholderText(/e\.g\. Lufthansa/i), 'Grand Hotel');
    await userEvent.click(screen.getByRole('button', { name: /^Add$/i }));

    await waitFor(() => expect(onSave).toHaveBeenCalled());
    expect(onSave).toHaveBeenCalledWith(
      expect.objectContaining({ title: 'Grand Hotel', type: 'hotel' })
    );
  });

  it('FE-PLANNER-RESMODAL-031: event type — saving calls onSave with event type', async () => {
    const onSave = vi.fn().mockResolvedValue(undefined);
    render(<ReservationModal {...defaultProps} onSave={onSave} />);

    await userEvent.click(screen.getByRole('button', { name: /Event/i }));
    await userEvent.type(screen.getByPlaceholderText(/e\.g\. Lufthansa/i), 'Louvre Museum');
    await userEvent.click(screen.getByRole('button', { name: /^Add$/i }));

    await waitFor(() => expect(onSave).toHaveBeenCalled());
    expect(onSave).toHaveBeenCalledWith(
      expect.objectContaining({ title: 'Louvre Museum', type: 'event' })
    );
  });

  it('FE-PLANNER-RESMODAL-032: edit mode — save button shows "Update"', () => {
    const res = buildReservation({ title: 'My Trip', type: 'other' });
    render(<ReservationModal {...defaultProps} reservation={res} />);
    expect(screen.getByRole('button', { name: /^Update$/i })).toBeInTheDocument();
  });

  it('FE-PLANNER-RESMODAL-033: modal is closed when isOpen=false', () => {
    render(<ReservationModal {...defaultProps} isOpen={false} />);
    // When isOpen=false the Modal component should hide content
    expect(screen.queryByText(/New Reservation/i)).not.toBeInTheDocument();
  });

  it('FE-PLANNER-RESMODAL-034: location and confirmation number inputs are present', () => {
    render(<ReservationModal {...defaultProps} />);
    expect(screen.getByPlaceholderText(/Address, Airport/i)).toBeInTheDocument();
    expect(screen.getByPlaceholderText(/e\.g\. ABC12345/i)).toBeInTheDocument();
  });

  it('FE-PLANNER-RESMODAL-036: file upload to existing reservation calls onFileUpload', async () => {
    const onFileUpload = vi.fn().mockResolvedValue(undefined);
    const res = buildReservation({ id: 10, title: 'My Trip', type: 'other' });
    render(
      <ReservationModal
        {...defaultProps}
        reservation={res}
        onFileUpload={onFileUpload}
      />
    );

    const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
    const testFile = new File(['content'], 'boarding-pass.pdf', { type: 'application/pdf' });
    fireEvent.change(fileInput, { target: { files: [testFile] } });

    await waitFor(() => expect(onFileUpload).toHaveBeenCalled());
    const [fd] = onFileUpload.mock.calls[0] as [FormData];
    expect(fd.get('file')).toBeTruthy();
    // FormData.append coerces numbers to strings
    expect(fd.get('reservation_id')).toBe('10');
  });

  it('FE-PLANNER-RESMODAL-037: link existing file button appears when unattached files exist', () => {
    const res = buildReservation({ id: 5 });
    // File NOT attached to this reservation
    const unattachedFile = buildTripFile({ id: 99, original_name: 'invoice.pdf' });

    render(
      <ReservationModal
        {...defaultProps}
        reservation={res}
        files={[unattachedFile]}
      />
    );

    expect(screen.getByRole('button', { name: /Link existing file/i })).toBeInTheDocument();
  });

  it('FE-PLANNER-RESMODAL-038: clicking "link existing file" shows file picker dropdown', async () => {
    const res = buildReservation({ id: 5 });
    const unattachedFile = buildTripFile({ id: 99, original_name: 'invoice.pdf' });

    render(
      <ReservationModal
        {...defaultProps}
        reservation={res}
        files={[unattachedFile]}
      />
    );

    await userEvent.click(screen.getByRole('button', { name: /Link existing file/i }));
    expect(screen.getByText('invoice.pdf')).toBeInTheDocument();
  });

  it('FE-PLANNER-RESMODAL-039: clicking file in picker links it and closes picker', async () => {
    server.use(
      http.post('/api/trips/1/files/99/link', () => HttpResponse.json({ success: true })),
      http.get('/api/trips/1/files', () => HttpResponse.json({ files: [] })),
    );

    const res = buildReservation({ id: 5 });
    const unattachedFile = buildTripFile({ id: 99, original_name: 'invoice.pdf' });

    render(
      <ReservationModal
        {...defaultProps}
        reservation={res}
        files={[unattachedFile]}
      />
    );

    await userEvent.click(screen.getByRole('button', { name: /Link existing file/i }));
    await userEvent.click(screen.getByText('invoice.pdf'));

    // After linking, the file is moved to attached files and the "Link existing file" button disappears
    // (all files are now attached, so the picker condition becomes false)
    await waitFor(() => {
      expect(screen.queryByRole('button', { name: /Link existing file/i })).not.toBeInTheDocument();
    });
  });

  it('FE-PLANNER-RESMODAL-040: removing pending file removes it from list', async () => {
    render(<ReservationModal {...defaultProps} reservation={null} />);

    const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
    const testFile = new File(['content'], 'draft.pdf', { type: 'application/pdf' });
    fireEvent.change(fileInput, { target: { files: [testFile] } });

    await waitFor(() => expect(screen.getByText('draft.pdf')).toBeInTheDocument());

    // Click the X next to the pending file
    const removeButtons = screen.getAllByRole('button');
    const pendingFileRow = screen.getByText('draft.pdf').closest('div')!;
    const removeBtn = pendingFileRow.querySelector('button')!;
    await userEvent.click(removeBtn);

    await waitFor(() => expect(screen.queryByText('draft.pdf')).not.toBeInTheDocument());
  });

  it('FE-PLANNER-RESMODAL-041: budget section not shown when addon disabled', () => {
    render(<ReservationModal {...defaultProps} />);
    expect(screen.queryByPlaceholderText('0.00')).not.toBeInTheDocument();
  });

  it('FE-PLANNER-RESMODAL-042: hotel type metadata saved with check-in time', async () => {
    const onSave = vi.fn().mockResolvedValue(undefined);
    render(<ReservationModal {...defaultProps} onSave={onSave} />);

    await userEvent.click(screen.getByRole('button', { name: /Accommodation/i }));
    await userEvent.type(screen.getByPlaceholderText(/e\.g\. Lufthansa/i), 'Grand Hotel');

    await userEvent.click(screen.getByRole('button', { name: /^Add$/i }));

    await waitFor(() => expect(onSave).toHaveBeenCalled());
    expect(onSave).toHaveBeenCalledWith(
      expect.objectContaining({ title: 'Grand Hotel', type: 'hotel' })
    );
  });

  it('FE-PLANNER-RESMODAL-043: hover styles applied to file picker items', async () => {
    const res = buildReservation({ id: 5 });
    const unattachedFile = buildTripFile({ id: 99, original_name: 'invoice.pdf' });

    render(
      <ReservationModal
        {...defaultProps}
        reservation={res}
        files={[unattachedFile]}
      />
    );

    await userEvent.click(screen.getByRole('button', { name: /Link existing file/i }));
    const filePickerItem = screen.getByText('invoice.pdf').closest('button')!;
    fireEvent.mouseEnter(filePickerItem);
    fireEvent.mouseLeave(filePickerItem);
    // Just testing the handlers don't throw
    expect(filePickerItem).toBeInTheDocument();
  });

  it('FE-PLANNER-RESMODAL-044: budget category dropdown options include existing categories', () => {
    seedStore(useAddonStore, {
      addons: [{ id: 'budget', name: 'Budget', type: 'budget', icon: '', enabled: true }],
      loaded: true,
    });
    seedStore(useTripStore, {
      trip: buildTrip({ id: 1 }),
      budgetItems: [
        { id: 1, trip_id: 1, name: 'Flight ticket', amount: 300, currency: 'EUR', category: 'Transport', paid_by: null, persons: 1, members: [], expense_date: null },
      ],
    });
    render(<ReservationModal {...defaultProps} />);
    // Budget section is visible
    expect(screen.getByText(/Budget category/i)).toBeInTheDocument();
  });

  it('FE-PLANNER-RESMODAL-045: tour type shows time pickers', async () => {
    render(<ReservationModal {...defaultProps} />);
    await userEvent.click(screen.getByRole('button', { name: /^Tour$/i }));
    await waitFor(() => {
      expect(screen.getAllByTestId('time-picker').length).toBeGreaterThan(0);
    });
  });

  it('FE-PLANNER-RESMODAL-046: other type renders and saves correctly', async () => {
    const onSave = vi.fn().mockResolvedValue(undefined);
    render(<ReservationModal {...defaultProps} onSave={onSave} />);
    await userEvent.click(screen.getByRole('button', { name: /^Other$/i }));
    await userEvent.type(screen.getByPlaceholderText(/e\.g\. Lufthansa/i), 'Misc item');
    await userEvent.click(screen.getByRole('button', { name: /^Add$/i }));
    await waitFor(() => expect(onSave).toHaveBeenCalledWith(expect.objectContaining({ type: 'other' })));
  });

  it('FE-PLANNER-RESMODAL-047: clicking budget category select changes the value', async () => {
    seedStore(useAddonStore, {
      addons: [{ id: 'budget', name: 'Budget', type: 'budget', icon: '', enabled: true }],
      loaded: true,
    });
    seedStore(useTripStore, {
      trip: buildTrip({ id: 1 }),
      budgetItems: [
        { id: 1, trip_id: 1, name: 'Ticket', amount: 100, currency: 'EUR', category: 'Transport', paid_by: null, persons: 1, members: [], expense_date: null },
      ],
    });
    render(<ReservationModal {...defaultProps} />);

    // Open the budget category CustomSelect (shows placeholder "Auto (from booking type)")
    const budgetCategoryBtn = screen.getByText(/Auto \(from booking type\)/i).closest('button')!;
    await userEvent.click(budgetCategoryBtn);

    // Click the "Transport" category option
    await waitFor(() => expect(screen.getByText('Transport')).toBeInTheDocument());
    await userEvent.click(screen.getByText('Transport'));

    // The select should now show "Transport"
    expect(screen.getByText('Transport')).toBeInTheDocument();
  });

  it('FE-PLANNER-RESMODAL-048: clicking attach file button triggers file input', async () => {
    render(<ReservationModal {...defaultProps} />);
    const attachBtn = screen.getByRole('button', { name: /Attach file/i });
    // Mock click on hidden file input
    const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
    const clickSpy = vi.spyOn(fileInput, 'click').mockImplementation(() => {});
    await userEvent.click(attachBtn);
    expect(clickSpy).toHaveBeenCalled();
    clickSpy.mockRestore();
  });

  it('FE-PLANNER-RESMODAL-049: unlinking a linked file removes it from attached list', async () => {
    // First link the file, then unlink it via the X button
    server.use(
      http.post('/api/trips/1/files/42/link', () => HttpResponse.json({ success: true })),
      http.get('/api/trips/1/files/42/links', () => HttpResponse.json({ links: [{ id: 1, reservation_id: 7 }] })),
      http.delete('/api/trips/1/files/42/link/1', () => HttpResponse.json({ success: true })),
      http.get('/api/trips/1/files', () => HttpResponse.json({ files: [] })),
    );

    const res = buildReservation({ id: 7 });
    // File is NOT attached (no reservation_id) — it will be in the "link existing" picker
    const looseFile = buildTripFile({ id: 42, original_name: 'receipt.pdf' });

    render(
      <ReservationModal
        {...defaultProps}
        reservation={res}
        files={[looseFile]}
      />
    );

    // Link the file via the picker
    await userEvent.click(screen.getByRole('button', { name: /Link existing file/i }));
    await waitFor(() => expect(screen.getByText('receipt.pdf')).toBeInTheDocument());
    await userEvent.click(screen.getByText('receipt.pdf'));

    // File is now in attached list; "Link existing file" button gone
    await waitFor(() =>
      expect(screen.queryByRole('button', { name: /Link existing file/i })).not.toBeInTheDocument()
    );

    // Click the X to unlink
    const fileRow = screen.getByText('receipt.pdf').closest('div')!;
    const unlinkBtn = fileRow.querySelector('button[type="button"]')!;
    await userEvent.click(unlinkBtn);

    // File removed from attached list and "Link existing file" button reappears
    await waitFor(() => {
      expect(screen.getByRole('button', { name: /Link existing file/i })).toBeInTheDocument();
    });
  });

  it('FE-PLANNER-RESMODAL-035: hotel type saves correctly', async () => {
    const onSave = vi.fn().mockResolvedValue(undefined);
    render(<ReservationModal {...defaultProps} onSave={onSave} />);

    await userEvent.click(screen.getByRole('button', { name: /^Accommodation$/i }));
    await userEvent.type(screen.getByPlaceholderText(/e\.g\. Lufthansa/i), 'Hotel Test');
    await userEvent.click(screen.getByRole('button', { name: /^Add$/i }));

    await waitFor(() => expect(onSave).toHaveBeenCalled());
    expect(onSave).toHaveBeenCalledWith(
      expect.objectContaining({ type: 'hotel' })
    );
  });

  // ── Hotel day-range picker — non-monotonic IDs (issue #929) ───────────────
  // Mirrors DayDetailPanel-056/057 for the ReservationModal path.
  // ID layout: day_number 1-9 → IDs 17-25, day_number 10-16 → IDs 1-7.

  function buildNonMonotonicDaysRM() {
    return [
      buildDay({ id: 17, trip_id: 1, date: '2026-04-30', day_number: 1 }),
      buildDay({ id: 18, trip_id: 1, date: '2026-05-01', day_number: 2 }),
      buildDay({ id: 19, trip_id: 1, date: '2026-05-02', day_number: 3 }),
      buildDay({ id: 20, trip_id: 1, date: '2026-05-03', day_number: 4 }),
      buildDay({ id: 21, trip_id: 1, date: '2026-05-04', day_number: 5 }),
      buildDay({ id: 22, trip_id: 1, date: '2026-05-05', day_number: 6 }),
      buildDay({ id: 23, trip_id: 1, date: '2026-05-06', day_number: 7 }),
      buildDay({ id: 24, trip_id: 1, date: '2026-05-07', day_number: 8 }),
      buildDay({ id: 25, trip_id: 1, date: '2026-05-08', day_number: 9 }),
      buildDay({ id: 1,  trip_id: 1, date: '2026-05-09', day_number: 10 }),
      buildDay({ id: 2,  trip_id: 1, date: '2026-05-10', day_number: 11 }),
      buildDay({ id: 3,  trip_id: 1, date: '2026-05-11', day_number: 12 }),
      buildDay({ id: 4,  trip_id: 1, date: '2026-05-12', day_number: 13 }),
      buildDay({ id: 5,  trip_id: 1, date: '2026-05-13', day_number: 14 }),
      buildDay({ id: 6,  trip_id: 1, date: '2026-05-14', day_number: 15 }),
      buildDay({ id: 7,  trip_id: 1, date: '2026-05-15', day_number: 16 }),
    ] as any[];
  }

  it('FE-PLANNER-RESMODAL-050: non-monotonic IDs — end picker with low ID does not clobber start', async () => {
    const onSave = vi.fn().mockResolvedValue(undefined);
    const days = buildNonMonotonicDaysRM();

    render(<ReservationModal {...defaultProps} onSave={onSave} days={days} />);

    // Switch to hotel type
    await userEvent.click(screen.getByRole('button', { name: /^Accommodation$/i }));
    await userEvent.type(screen.getByPlaceholderText(/e\.g\. Lufthansa/i), 'Overlap Hotel');

    // Open start picker (first "Select day" trigger) and select Day 1 (id=17)
    const startTrigger = () => screen.getAllByRole('button').filter(b => b.textContent?.includes('Select day') || b.textContent?.startsWith('Day '))[0];
    await userEvent.click(startTrigger());
    await userEvent.click(screen.getAllByRole('button').find(b => b.textContent?.startsWith('Day 1') && !b.textContent?.startsWith('Day 1 ') || b.textContent?.trim() === 'Day 1')!);

    // Open end picker and select Day 16 (id=7, low ID but last positionally)
    const endTrigger = () => screen.getAllByRole('button').filter(b => b.textContent?.includes('Select day') || /^Day \d+/.test(b.textContent?.trim() ?? ''))[1];
    await userEvent.click(endTrigger());
    await userEvent.click(screen.getAllByRole('button').find(b => b.textContent?.startsWith('Day 16'))!);

    await userEvent.click(screen.getByRole('button', { name: /^Add$/i }));

    await waitFor(() => expect(onSave).toHaveBeenCalled());
    const saved = onSave.mock.calls[0][0];
    // start must stay id=17 (Day 1) — old Math.max would clobber it to id=7
    expect(saved.create_accommodation?.start_day_id).toBe(17);
    expect(saved.create_accommodation?.end_day_id).toBe(7);
  });

  it('FE-PLANNER-RESMODAL-051: non-monotonic IDs — start picker does not collapse end when start has high ID', async () => {
    const onSave = vi.fn().mockResolvedValue(undefined);
    const days = buildNonMonotonicDaysRM();

    render(<ReservationModal {...defaultProps} onSave={onSave} days={days} />);

    await userEvent.click(screen.getByRole('button', { name: /^Accommodation$/i }));
    await userEvent.type(screen.getByPlaceholderText(/e\.g\. Lufthansa/i), 'Span Hotel');

    // Set end to Day 16 (id=7) first
    const endTrigger = () => screen.getAllByRole('button').filter(b => b.textContent?.includes('Select day') || /^Day \d+/.test(b.textContent?.trim() ?? ''))[1];
    await userEvent.click(endTrigger());
    await userEvent.click(screen.getAllByRole('button').find(b => b.textContent?.startsWith('Day 16'))!);

    // Set start to Day 9 (id=25, high ID but earlier by position than Day 16)
    // Old code: Math.max(25, 7) = 25 → end collapses to Day 9.
    // New code: position(id=25)=8 < position(id=7)=15 → end stays id=7.
    const startTrigger = () => screen.getAllByRole('button').filter(b => b.textContent?.includes('Select day') || /^Day \d+/.test(b.textContent?.trim() ?? ''))[0];
    await userEvent.click(startTrigger());
    await userEvent.click(screen.getAllByRole('button').find(b => b.textContent?.startsWith('Day 9'))!);

    await userEvent.click(screen.getByRole('button', { name: /^Add$/i }));

    await waitFor(() => expect(onSave).toHaveBeenCalled());
    const saved = onSave.mock.calls[0][0];
    expect(saved.create_accommodation?.start_day_id).toBe(25); // Day 9
    expect(saved.create_accommodation?.end_day_id).toBe(7);    // Day 16 — must NOT have collapsed
  });

  it('FE-PLANNER-RESMODAL-052: hotel with no accommodation_id sends assignment_id as null (issue #934)', async () => {
    const onSave = vi.fn().mockResolvedValue(undefined);
    // Hotel reservation with assignment_id set but no accommodation
    const res = buildReservation({
      id: 10, title: 'Stale Hotel', type: 'hotel', status: 'confirmed',
      accommodation_id: null, assignment_id: 99,
    } as any);

    render(<ReservationModal {...defaultProps} onSave={onSave} reservation={res} />);

    await userEvent.click(screen.getByRole('button', { name: /^Update$/i }));

    await waitFor(() => expect(onSave).toHaveBeenCalled());
    expect(onSave.mock.calls[0][0].assignment_id).toBeNull();
  });
});
