import { render, screen, waitFor, fireEvent, act } from '../../../tests/helpers/render';
import userEvent from '@testing-library/user-event';
import { buildUser, buildTrip, buildPlace, buildCategory, buildReservation } from '../../../tests/helpers/factories';
import { resetAllStores, seedStore } from '../../../tests/helpers/store';
import { useAuthStore } from '../../store/authStore';
import { useTripStore } from '../../store/tripStore';
import { useSettingsStore } from '../../store/settingsStore';

// ── Module mocks ──────────────────────────────────────────────────────────────

vi.mock('../../api/client', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../../api/client')>();
  return {
    ...actual,
    mapsApi: { details: vi.fn().mockResolvedValue({ place: null }) },
  };
});

vi.mock('../../api/authUrl', () => ({
  getAuthUrl: vi.fn().mockResolvedValue('http://test/file'),
}));

vi.mock('../../services/photoService', () => ({
  getCached: vi.fn(() => null),
  isLoading: vi.fn(() => false),
  fetchPhoto: vi.fn(),
  onThumbReady: vi.fn(() => () => {}),
}));

// ── IntersectionObserver stub ─────────────────────────────────────────────────

class MockIO {
  observe = vi.fn();
  disconnect = vi.fn();
  unobserve = vi.fn();
}

beforeAll(() => {
  (globalThis as any).IntersectionObserver = MockIO;
});

// ── Import component after mocks ──────────────────────────────────────────────

import PlaceInspector from './PlaceInspector';
import { mapsApi } from '../../api/client';

// ── Shared fixtures ───────────────────────────────────────────────────────────

const place = buildPlace({
  id: 1,
  name: 'Eiffel Tower',
  address: 'Champ de Mars, Paris',
  lat: 48.8584,
  lng: 2.2945,
  description: 'Famous iron tower',
});

const cat = buildCategory({ name: 'Landmark', icon: 'MapPin' });

const defaultProps = {
  place,
  categories: [cat],
  days: [],
  selectedDayId: null as number | null,
  selectedAssignmentId: null as number | null,
  assignments: {} as Record<string, any[]>,
  reservations: [] as any[],
  onClose: vi.fn(),
  onEdit: vi.fn(),
  onDelete: vi.fn(),
  onAssignToDay: vi.fn(),
  onRemoveAssignment: vi.fn(),
  files: [] as any[],
  onFileUpload: vi.fn().mockResolvedValue(undefined),
  tripMembers: [] as any[],
  onSetParticipants: vi.fn(),
  onUpdatePlace: vi.fn(),
};

// ── Setup / teardown ──────────────────────────────────────────────────────────

beforeEach(() => {
  resetAllStores();
  vi.clearAllMocks();
  sessionStorage.clear();

  seedStore(useAuthStore, { user: buildUser(), isAuthenticated: true });
  seedStore(useTripStore, { trip: buildTrip({ id: 1 }) });
  seedStore(useSettingsStore, { settings: { time_format: '24h', temperature_unit: 'celsius' } });

  vi.mocked(mapsApi.details).mockResolvedValue({ place: null });
});

// ── Tests ─────────────────────────────────────────────────────────────────────

describe('PlaceInspector', () => {

  // ── Rendering ──────────────────────────────────────────────────────────────

  it('FE-PLANNER-INSPECTOR-001: returns null when place is null', () => {
    const { container } = render(<PlaceInspector {...defaultProps} place={null} />);
    expect(container.firstChild).toBeNull();
  });

  it('FE-PLANNER-INSPECTOR-002: renders without crashing with a valid place', () => {
    render(<PlaceInspector {...defaultProps} />);
    expect(document.body).toBeTruthy();
  });

  it('FE-PLANNER-INSPECTOR-003: shows place name in header', () => {
    render(<PlaceInspector {...defaultProps} />);
    expect(screen.getByText('Eiffel Tower')).toBeTruthy();
  });

  it('FE-PLANNER-INSPECTOR-004: shows place address', () => {
    render(<PlaceInspector {...defaultProps} />);
    expect(screen.getByText(/Champ de Mars, Paris/)).toBeTruthy();
  });

  it('FE-PLANNER-INSPECTOR-005: shows category badge with category name', () => {
    const placeWithCat = buildPlace({ id: 100, category_id: cat.id });
    render(<PlaceInspector {...defaultProps} place={placeWithCat} categories={[cat]} />);
    const matches = screen.getAllByText('Landmark');
    expect(matches.length).toBeGreaterThan(0);
  });

  it('FE-PLANNER-INSPECTOR-006: shows lat/lng coordinates', () => {
    render(<PlaceInspector {...defaultProps} />);
    // The component renders Number(lat).toFixed(6), Number(lng).toFixed(6)
    expect(screen.getByText(/48\.858400/)).toBeTruthy();
    expect(screen.getByText(/2\.294500/)).toBeTruthy();
  });

  it('FE-PLANNER-INSPECTOR-007: shows time range when place_time and end_time are set', () => {
    const p = buildPlace({ id: 101, place_time: '09:00', end_time: '17:00' });
    render(<PlaceInspector {...defaultProps} place={p} />);
    expect(screen.getByText(/09:00/)).toBeTruthy();
    expect(screen.getByText(/17:00/)).toBeTruthy();
  });

  it('FE-PLANNER-INSPECTOR-008: shows only start time when no end_time', () => {
    const p = buildPlace({ id: 102, place_time: '09:00', end_time: null });
    render(<PlaceInspector {...defaultProps} place={p} />);
    expect(screen.getByText(/09:00/)).toBeTruthy();
    // The '–' separator should not be present
    expect(screen.queryByText(/–/)).toBeNull();
  });

  it('FE-PLANNER-INSPECTOR-009: description is rendered as markdown', () => {
    const p = buildPlace({ id: 103, description: '**Bold text**' });
    const { container } = render(<PlaceInspector {...defaultProps} place={p} />);
    const strong = container.querySelector('strong');
    expect(strong).toBeTruthy();
    expect(strong?.textContent).toBe('Bold text');
  });

  it('FE-PLANNER-INSPECTOR-010: notes rendered when no description', () => {
    const p = buildPlace({ id: 104, description: null, notes: 'Some notes' } as any);
    render(<PlaceInspector {...defaultProps} place={p} />);
    expect(screen.getByText(/Some notes/)).toBeTruthy();
  });

  // ── Close button ───────────────────────────────────────────────────────────

  it('FE-PLANNER-INSPECTOR-011: close (X) button calls onClose', async () => {
    const user = userEvent.setup();
    const onClose = vi.fn();
    render(<PlaceInspector {...defaultProps} onClose={onClose} />);
    // Find the X button — it's the close button with an X icon inside
    const buttons = screen.getAllByRole('button');
    // The close button is typically in the header, first button with X icon
    const closeBtn = buttons.find(btn => btn.querySelector('svg'));
    // Click the last-found header button that has no text label (the X)
    // More reliable: find button by its position as close button
    await user.click(buttons[0]); // first button is the close X
    expect(onClose).toHaveBeenCalled();
  });

  // ── Edit / Delete buttons ──────────────────────────────────────────────────

  it('FE-PLANNER-INSPECTOR-012: Edit button is visible', () => {
    render(<PlaceInspector {...defaultProps} />);
    // Edit button is in footer actions
    const buttons = screen.getAllByRole('button');
    expect(buttons.length).toBeGreaterThan(0);
  });

  it('FE-PLANNER-INSPECTOR-013: clicking Edit button calls onEdit', async () => {
    const user = userEvent.setup();
    const onEdit = vi.fn();
    const { container } = render(<PlaceInspector {...defaultProps} onEdit={onEdit} />);
    // The edit button has Edit2 icon — find footer buttons
    const allButtons = screen.getAllByRole('button');
    // Edit button is second-to-last in footer (before delete)
    const editBtn = allButtons[allButtons.length - 2];
    await user.click(editBtn);
    expect(onEdit).toHaveBeenCalled();
  });

  it('FE-PLANNER-INSPECTOR-014: clicking Delete button calls onDelete', async () => {
    const user = userEvent.setup();
    const onDelete = vi.fn();
    render(<PlaceInspector {...defaultProps} onDelete={onDelete} />);
    const allButtons = screen.getAllByRole('button');
    // Delete button is the last button in the footer
    const deleteBtn = allButtons[allButtons.length - 1];
    await user.click(deleteBtn);
    expect(onDelete).toHaveBeenCalled();
  });

  // ── Assign to / remove from day ────────────────────────────────────────────

  it('FE-PLANNER-INSPECTOR-015: "Add to day" button appears when selectedDayId is set and place NOT in that day', () => {
    render(<PlaceInspector {...defaultProps} selectedDayId={1} assignments={{ '1': [] }} />);
    const allButtons = screen.getAllByRole('button');
    // The add-to-day button is the first footer button (Plus icon)
    // It should exist when selectedDayId is set and place is not assigned
    expect(allButtons.length).toBeGreaterThan(2);
  });

  it('FE-PLANNER-INSPECTOR-016: clicking assign-to-day button calls onAssignToDay with placeId', async () => {
    const user = userEvent.setup();
    const onAssignToDay = vi.fn();
    render(
      <PlaceInspector
        {...defaultProps}
        selectedDayId={1}
        assignments={{ '1': [] }}
        onAssignToDay={onAssignToDay}
      />
    );
    const addBtn = screen.getByText('Add to Day').closest('button')!;
    await user.click(addBtn);
    expect(onAssignToDay).toHaveBeenCalledWith(place.id);
  });

  it('FE-PLANNER-INSPECTOR-017: "Remove from day" button appears when place IS assigned to selectedDay', () => {
    const assignmentInDay = [{ id: 99, place, day_id: 1, place_id: place.id, order_index: 0, notes: null }];
    render(
      <PlaceInspector
        {...defaultProps}
        selectedDayId={1}
        assignments={{ '1': assignmentInDay }}
      />
    );
    const allButtons = screen.getAllByRole('button');
    expect(allButtons.length).toBeGreaterThan(2);
  });

  it('FE-PLANNER-INSPECTOR-018: clicking remove calls onRemoveAssignment with dayId and assignmentId', async () => {
    const user = userEvent.setup();
    const onRemoveAssignment = vi.fn();
    const assignmentInDay = [{ id: 99, place, day_id: 1, place_id: place.id, order_index: 0, notes: null }];
    render(
      <PlaceInspector
        {...defaultProps}
        selectedDayId={1}
        assignments={{ '1': assignmentInDay }}
        onRemoveAssignment={onRemoveAssignment}
      />
    );
    // Find the remove button — it has "Remove" text (sm:hidden span)
    const removeBtn = screen.getByText('Remove').closest('button')!;
    await user.click(removeBtn);
    // Component calls onRemoveAssignment(selectedDayId, assignmentInDay.id)
    expect(onRemoveAssignment).toHaveBeenCalledWith(1, 99);
  });

  // ── Inline name editing ────────────────────────────────────────────────────

  it('FE-PLANNER-INSPECTOR-019: double-clicking name enters edit mode', async () => {
    const user = userEvent.setup();
    render(<PlaceInspector {...defaultProps} />);
    const nameSpan = screen.getByText('Eiffel Tower');
    await user.dblClick(nameSpan);
    const input = screen.getByDisplayValue('Eiffel Tower');
    expect(input).toBeTruthy();
  });

  it('FE-PLANNER-INSPECTOR-020: pressing Enter commits edit and calls onUpdatePlace', async () => {
    const user = userEvent.setup();
    const onUpdatePlace = vi.fn();
    render(<PlaceInspector {...defaultProps} onUpdatePlace={onUpdatePlace} />);
    const nameSpan = screen.getByText('Eiffel Tower');
    await user.dblClick(nameSpan);
    const input = screen.getByDisplayValue('Eiffel Tower');
    await user.clear(input);
    await user.type(input, 'New Tower Name');
    await user.keyboard('{Enter}');
    expect(onUpdatePlace).toHaveBeenCalledWith(place.id, { name: 'New Tower Name' });
  });

  it('FE-PLANNER-INSPECTOR-021: pressing Escape cancels edit', async () => {
    const user = userEvent.setup();
    render(<PlaceInspector {...defaultProps} />);
    const nameSpan = screen.getByText('Eiffel Tower');
    await user.dblClick(nameSpan);
    expect(screen.getByDisplayValue('Eiffel Tower')).toBeTruthy();
    await user.keyboard('{Escape}');
    expect(screen.queryByDisplayValue('Eiffel Tower')).toBeNull();
    expect(screen.getByText('Eiffel Tower')).toBeTruthy();
  });

  it('FE-PLANNER-INSPECTOR-022: blank name does not call onUpdatePlace', async () => {
    const user = userEvent.setup();
    const onUpdatePlace = vi.fn();
    render(<PlaceInspector {...defaultProps} onUpdatePlace={onUpdatePlace} />);
    const nameSpan = screen.getByText('Eiffel Tower');
    await user.dblClick(nameSpan);
    const input = screen.getByDisplayValue('Eiffel Tower');
    await user.clear(input);
    await user.keyboard('{Enter}');
    expect(onUpdatePlace).not.toHaveBeenCalled();
  });

  // ── Google Maps details (mapsApi) ──────────────────────────────────────────

  it('FE-PLANNER-INSPECTOR-023: mapsApi.details called when place has google_place_id', async () => {
    const p = buildPlace({ id: 200, google_place_id: 'ChIJ001' });
    render(<PlaceInspector {...defaultProps} place={p} />);
    await waitFor(() => {
      expect(vi.mocked(mapsApi.details)).toHaveBeenCalledWith('ChIJ001', expect.any(String));
    });
  });

  it('FE-PLANNER-INSPECTOR-024: rating chip shown when googleDetails has rating', async () => {
    vi.mocked(mapsApi.details).mockResolvedValue({
      place: { rating: 4.5, rating_count: 1200 },
    } as any);
    const p = buildPlace({ id: 201, google_place_id: 'ChIJ002' });
    render(<PlaceInspector {...defaultProps} place={p} />);
    await screen.findByText(/4\.5/);
  });

  it('FE-PLANNER-INSPECTOR-025: opening hours shown when available', async () => {
    vi.mocked(mapsApi.details).mockResolvedValue({
      place: { opening_hours: ['Mon: 9:00 AM – 5:00 PM', 'Tue: 9:00 AM – 5:00 PM'] },
    } as any);
    const user = userEvent.setup();
    const p = buildPlace({ id: 202, google_place_id: 'ChIJ003' });
    render(<PlaceInspector {...defaultProps} place={p} />);
    // Wait for hours to load — the button text shows a day's hours line
    const hoursBtn = await screen.findByText(/Show opening hours|Opening Hours|Mon:|9:00|09:00/i);
    const btn = hoursBtn.closest('button')!;
    await user.click(btn);
    // After expand, one of the hours lines should be visible
    await waitFor(() => {
      expect(screen.getByText(/Mon:/)).toBeTruthy();
    });
  });

  it('FE-PLANNER-INSPECTOR-026: open/closed badge shown when open_now is available', async () => {
    vi.mocked(mapsApi.details).mockResolvedValue({
      place: { open_now: true },
    } as any);
    const p = buildPlace({ id: 203, google_place_id: 'ChIJ004' });
    render(<PlaceInspector {...defaultProps} place={p} />);
    await screen.findByText(/open/i);
  });

  it('FE-PLANNER-INSPECTOR-027: mapsApi.details NOT called when place has no google_place_id or osm_id', async () => {
    const p = buildPlace({ id: 204, google_place_id: null, osm_id: null });
    render(<PlaceInspector {...defaultProps} place={p} />);
    // Wait a tick
    await act(async () => { await new Promise(r => setTimeout(r, 50)) });
    expect(vi.mocked(mapsApi.details)).not.toHaveBeenCalled();
  });

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

  it('FE-PLANNER-INSPECTOR-028: files section shows file names after expanding', async () => {
    const user = userEvent.setup();
    const file = {
      id: 1,
      trip_id: 1,
      place_id: place.id,
      original_name: 'photo.jpg',
      url: '/uploads/photo.jpg',
      filename: 'photo.jpg',
      mime_type: 'image/jpeg',
      file_size: 1024,
      created_at: '2025-01-01T00:00:00.000Z',
    };
    render(<PlaceInspector {...defaultProps} files={[file as any]} />);
    // The files section header/toggle is always visible; click to expand
    const allButtons = screen.getAllByRole('button');
    const filesBtn = allButtons.find(btn => btn.textContent?.includes('1'));
    // Click the expand button (file count label button)
    if (filesBtn) {
      await user.click(filesBtn);
      await screen.findByText('photo.jpg');
    } else {
      // Try clicking the last non-footer button
      const toggleButtons = allButtons.filter(btn => !btn.closest('footer'));
      await user.click(toggleButtons[0]);
    }
  });

  it('FE-PLANNER-INSPECTOR-029: hidden file input is present when onFileUpload provided', () => {
    const { container } = render(<PlaceInspector {...defaultProps} />);
    const fileInput = container.querySelector('input[type="file"]');
    expect(fileInput).toBeTruthy();
  });

  // ── Reservation chip ───────────────────────────────────────────────────────

  it('FE-PLANNER-INSPECTOR-030: linked reservation shown when selectedAssignmentId has a reservation', () => {
    const reservation = buildReservation({ title: 'Museum Ticket', status: 'confirmed', assignment_id: 99 } as any);
    const assignmentInDay = [{ id: 99, place, day_id: 1, place_id: place.id, order_index: 0, notes: null }];
    render(
      <PlaceInspector
        {...defaultProps}
        selectedDayId={1}
        selectedAssignmentId={99}
        assignments={{ '1': assignmentInDay }}
        reservations={[reservation]}
      />
    );
    expect(screen.getByText('Museum Ticket')).toBeTruthy();
  });

  // ── Participants ───────────────────────────────────────────────────────────

  it('FE-PLANNER-INSPECTOR-031: participants section shown when tripMembers > 1 and selectedAssignmentId is set', () => {
    const members = [buildUser({ id: 1 }), buildUser({ id: 2 })];
    const assignmentInDay = [{ id: 99, place, day_id: 1, place_id: place.id, order_index: 0, notes: null }];
    render(
      <PlaceInspector
        {...defaultProps}
        tripMembers={members}
        selectedDayId={1}
        selectedAssignmentId={99}
        assignments={{ '1': assignmentInDay }}
      />
    );
    // The participants section renders with a "participants" label
    // It's visible when tripMembers.length > 1 && selectedAssignmentId is set
    expect(screen.getByText(members[0].username)).toBeTruthy();
  });

  // ── Price chip ─────────────────────────────────────────────────────────────

  it('FE-PLANNER-INSPECTOR-032: price chip shown when place.price > 0', () => {
    const p = buildPlace({ id: 300, price: 15, currency: 'EUR' } as any);
    render(<PlaceInspector {...defaultProps} place={p} />);
    expect(screen.getByText(/15 EUR/)).toBeTruthy();
  });

  // ── Phone number ───────────────────────────────────────────────────────────

  it('FE-PLANNER-INSPECTOR-033: phone number shown when place has phone', () => {
    const p = buildPlace({ id: 301, phone: '+33 1 23 45 67 89' } as any);
    render(<PlaceInspector {...defaultProps} place={p} />);
    expect(screen.getByText(/\+33 1 23 45 67 89/)).toBeTruthy();
  });

  // ── File size display ──────────────────────────────────────────────────────

  it('FE-PLANNER-INSPECTOR-034: file size displayed in KB for files < 1MB', async () => {
    const user = userEvent.setup();
    const file = {
      id: 2,
      trip_id: 1,
      place_id: place.id,
      original_name: 'doc.pdf',
      url: '/uploads/doc.pdf',
      filename: 'doc.pdf',
      mime_type: 'application/pdf',
      file_size: 2048,
      created_at: '2025-01-01T00:00:00.000Z',
    };
    render(<PlaceInspector {...defaultProps} files={[file as any]} />);
    // Click expand to see file details
    const expandBtn = screen.getAllByRole('button').find(b => b.textContent?.includes('1'));
    if (expandBtn) {
      await user.click(expandBtn);
      await waitFor(() => {
        expect(screen.getByText(/2\.0 KB/)).toBeTruthy();
      });
    }
  });

  it('FE-PLANNER-INSPECTOR-035: file size displayed in MB for files >= 1MB', async () => {
    const user = userEvent.setup();
    const file = {
      id: 3,
      trip_id: 1,
      place_id: place.id,
      original_name: 'video.mp4',
      url: '/uploads/video.mp4',
      filename: 'video.mp4',
      mime_type: 'video/mp4',
      file_size: 2 * 1024 * 1024,
      created_at: '2025-01-01T00:00:00.000Z',
    };
    render(<PlaceInspector {...defaultProps} files={[file as any]} />);
    const expandBtn = screen.getAllByRole('button').find(b => b.textContent?.includes('1'));
    if (expandBtn) {
      await user.click(expandBtn);
      await waitFor(() => {
        expect(screen.getByText(/2\.0 MB/)).toBeTruthy();
      });
    }
  });

  // ── GPX track stats ────────────────────────────────────────────────────────

  it('FE-PLANNER-INSPECTOR-036: GPX track stats shown when route_geometry has 2D points', () => {
    const pts = [[48.8584, 2.2945], [48.8600, 2.3000], [48.8620, 2.3050]];
    const p = buildPlace({ id: 302, route_geometry: JSON.stringify(pts) } as any);
    render(<PlaceInspector {...defaultProps} place={p} />);
    // Track distance should be visible (e.g. "x.x km" or "xxx m")
    const { container } = render(<PlaceInspector {...defaultProps} place={p} />);
    expect(container.querySelector('svg')).toBeTruthy();
  });

  it('FE-PLANNER-INSPECTOR-037: GPX track stats shown with 3D points (elevation data)', () => {
    const pts = [
      [48.8584, 2.2945, 100],
      [48.8600, 2.3000, 120],
      [48.8620, 2.3050, 110],
      [48.8640, 2.3100, 130],
    ];
    const p = buildPlace({ id: 303, route_geometry: JSON.stringify(pts) } as any);
    const { container } = render(<PlaceInspector {...defaultProps} place={p} />);
    // Elevation stats should show max elevation 130m
    expect(screen.getByText(/130 m/)).toBeTruthy();
  });

  // ── ParticipantsBox interactions ───────────────────────────────────────────

  it('FE-PLANNER-INSPECTOR-038: participants list shows member names', () => {
    const member1 = buildUser({ id: 10, username: 'alice' });
    const member2 = buildUser({ id: 11, username: 'bob' });
    const members = [member1, member2];
    const assignmentInDay = [{
      id: 99, place, day_id: 1, place_id: place.id, order_index: 0, notes: null,
      participants: [{ user_id: 10, username: 'alice' }],
    }];
    render(
      <PlaceInspector
        {...defaultProps}
        tripMembers={members}
        selectedDayId={1}
        selectedAssignmentId={99}
        assignments={{ '1': assignmentInDay }}
      />
    );
    // alice is a participant, should appear
    expect(screen.getByText('alice')).toBeTruthy();
  });

  it('FE-PLANNER-INSPECTOR-039: session storage cache prevents duplicate mapsApi calls', async () => {
    // Prime the session storage cache with language 'en' (default)
    sessionStorage.setItem('gdetails_ChIJ005_en', JSON.stringify({ rating: 3.0 }));
    const p = buildPlace({ id: 304, google_place_id: 'ChIJ005' });
    render(<PlaceInspector {...defaultProps} place={p} />);
    // Wait for effect to run
    await act(async () => { await new Promise(r => setTimeout(r, 50)) });
    // mapsApi.details should NOT have been called (cache hit)
    expect(vi.mocked(mapsApi.details)).not.toHaveBeenCalled();
    // Rating from cache should be visible
    await screen.findByText(/3\.0/);
  });

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

  it('FE-PLANNER-INSPECTOR-040: file input change triggers onFileUpload', async () => {
    const onFileUpload = vi.fn().mockResolvedValue(undefined);
    const { container } = render(<PlaceInspector {...defaultProps} onFileUpload={onFileUpload} />);
    const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement;
    expect(fileInput).toBeTruthy();
    const testFile = new File(['content'], 'test.txt', { type: 'text/plain' });
    await act(async () => {
      fireEvent.change(fileInput, { target: { files: [testFile] } });
    });
    await waitFor(() => {
      expect(onFileUpload).toHaveBeenCalled();
    });
  });

  // ── formatTime: 12h format ─────────────────────────────────────────────────

  it('FE-PLANNER-INSPECTOR-041: time shown in 12h format when setting is 12h', () => {
    seedStore(useSettingsStore, { settings: { time_format: '12h' } });
    const p = buildPlace({ id: 305, place_time: '14:30', end_time: null });
    render(<PlaceInspector {...defaultProps} place={p} />);
    // 14:30 in 12h = "2:30 PM"
    expect(screen.getByText(/2:30 PM/)).toBeTruthy();
  });

  // ── convertHoursLine: 24h→12h conversion ──────────────────────────────────

  it('FE-PLANNER-INSPECTOR-042: opening hours converted to 12h when setting is 12h', async () => {
    seedStore(useSettingsStore, { settings: { time_format: '12h' } });
    vi.mocked(mapsApi.details).mockResolvedValue({
      place: { opening_hours: ['Mon: 09:00 – 17:00'] },
    } as any);
    const user = userEvent.setup();
    const p = buildPlace({ id: 306, google_place_id: 'ChIJ006' });
    render(<PlaceInspector {...defaultProps} place={p} />);
    const hoursSpan = await screen.findByText(/9:00 AM|Show opening hours/i);
    const btn = hoursSpan.closest('button')!;
    await user.click(btn);
    await waitFor(() => {
      expect(screen.getByText(/9:00 AM/)).toBeTruthy();
    });
  });

  // ── Google Maps URL action ─────────────────────────────────────────────────

  it('FE-PLANNER-INSPECTOR-043: Google Maps lat/lng button visible when no google_maps_url', () => {
    render(<PlaceInspector {...defaultProps} />);
    // place has lat/lng so Google Maps button should appear with Navigation icon
    const allButtons = screen.getAllByRole('button');
    // Find button containing "Google Maps" text
    const mapsBtn = allButtons.find(btn => btn.textContent?.includes('Google Maps'));
    expect(mapsBtn).toBeTruthy();
  });

  // ── No files section when no upload handler and no files ──────────────────

  it('FE-PLANNER-INSPECTOR-044: files section hidden when no files and no onFileUpload', () => {
    const { container } = render(
      <PlaceInspector {...defaultProps} files={[]} onFileUpload={undefined} />
    );
    expect(container.querySelector('input[type="file"]')).toBeNull();
  });

  // ── Participants section hidden when tripMembers <= 1 ─────────────────────

  it('FE-PLANNER-INSPECTOR-045: participants section hidden when tripMembers has only 1 member', () => {
    const member = buildUser({ id: 1, username: 'solo' });
    render(
      <PlaceInspector
        {...defaultProps}
        tripMembers={[member]}
        selectedDayId={1}
        selectedAssignmentId={99}
        assignments={{ '1': [{ id: 99, place, day_id: 1, place_id: place.id, order_index: 0, notes: null }] }}
      />
    );
    // "solo" username might be visible from other parts but participants box should not render
    // The participants box renders a "users" icon — check it's absent
    const text = document.body.textContent || '';
    // No second member to display
    expect(screen.queryByText('Participants')).toBeNull();
  });

  // ── Scroll / overflow (issue #1195) ──────────────────────────────────────

  it('FE-PLANNER-INSPECTOR-046: content area is a bounded flex scroll region', () => {
    const longText = 'Lorem ipsum dolor sit amet. '.repeat(200);
    const p = buildPlace({ id: 200, description: longText, notes: longText } as any);
    render(<PlaceInspector {...defaultProps} place={p} />);
    const scroll = screen.getByTestId('inspector-scroll') as HTMLElement;
    expect(scroll.style.overflowY).toBe('auto');
    expect(scroll.style.minHeight).toBe('0px');
    // flex must allow the region to shrink/grow within the capped card
    expect(scroll.style.flex).not.toBe('');
    expect(scroll.style.flex).not.toBe('0 0 auto');
  });

  it('FE-PLANNER-INSPECTOR-047: long unbroken description wraps instead of clipping horizontally', () => {
    const longWord = 'https://example.com/' + 'a'.repeat(300);
    const p = buildPlace({ id: 201, description: longWord } as any);
    const { container } = render(<PlaceInspector {...defaultProps} place={p} />);
    const descDiv = container.querySelector('.collab-note-md') as HTMLElement;
    expect(descDiv).toBeTruthy();
    expect(descDiv.style.overflowWrap).toBe('anywhere');
    expect(descDiv.style.wordBreak).toBe('break-word');
  });

  it('FE-PLANNER-INSPECTOR-048: description/notes do not shrink so the card scrolls instead of clipping', () => {
    const longText = 'Lorem ipsum dolor sit amet. '.repeat(200);
    const p = buildPlace({ id: 202, description: longText, notes: longText } as any);
    const { container } = render(<PlaceInspector {...defaultProps} place={p} />);
    const notes = Array.from(container.querySelectorAll('.collab-note-md')) as HTMLElement[];
    // Both description and notes containers must keep their natural height
    // (flex-shrink: 0) — otherwise they compress inside the flex column and
    // overflow:hidden clips the text with no scroll (issue #1195).
    expect(notes.length).toBe(2);
    for (const el of notes) {
      expect(el.style.flexShrink).toBe('0');
    }
  });

});

