/**
 * Tests for RecordsPage (M2-T10).
 *
 * Coverage:
 * 1. Poo list renders from mocked apiClient GET /api/poo.
 * 2. Poo pagination: page 2 requests offset=100.
 * 3. Edit poo: clicking Edit opens the modal; form submit calls PATCH with raw (un-encoded)
 *    PK in the path params (openapi-fetch encodes once; we must not double-encode).
 * 4. Delete poo: clicking Delete opens confirmation; confirming calls DELETE and refreshes.
 * 5. Location list renders from mocked apiClient GET /api/locations.
 * 6. Location pagination: page 2 requests offset=100.
 * 7. Edit location: clicking Edit opens modal; form submit calls PATCH with raw PK params.
 * 8. Delete location: clicking Delete opens confirmation; confirming calls DELETE.
 * 9. Real-encoding regression: stub global fetch; verify actual URL uses single encoding
 *    (%3A present, %253A absent) for PKs containing colons.
 */

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { screen, waitFor, fireEvent } from '@testing-library/react'
import { renderWithProviders } from '../test-utils'
import { RecordsPage } from './RecordsPage'
import type { LocationRecord } from '../records'

// ---------------------------------------------------------------------------
// Fixtures
// ---------------------------------------------------------------------------

const POO_RECORD = {
  timestamp: '2026-06-12T10:00:00Z',
  status: 'done',
  latitude: 51.5,
  longitude: -0.1,
}

const POO_RECORD_2 = {
  timestamp: '2026-06-12T11:00:00Z',
  status: 'pending',
  latitude: 51.6,
  longitude: -0.2,
}

const LOCATION_RECORD: LocationRecord = {
  person: 'alice',
  datetime: '2026-06-12T09:00:00Z',
  latitude: 52.0,
  longitude: 1.0,
  altitude: 10,
}

// Build a page of 100 items (all identical except for timestamp offset).
function makePooPage(offset: number) {
  return Array.from({ length: 100 }, (_, i) => ({
    timestamp: `2026-06-12T${String(offset + i).padStart(2, '0')}:00:00Z`,
    status: 'done',
    latitude: 51.5,
    longitude: -0.1,
  }))
}

function makeLocationPage(offset: number): LocationRecord[] {
  return Array.from({ length: 100 }, (_, i) => ({
    person: 'alice',
    datetime: `2026-06-12T${String(offset + i).padStart(2, '0')}:00:00Z`,
    latitude: 52.0,
    longitude: 1.0,
    altitude: null,
  }))
}

// ---------------------------------------------------------------------------
// Mock apiClient
// ---------------------------------------------------------------------------

const mockGet = vi.fn()
const mockPatch = vi.fn()
const mockDelete = vi.fn()

vi.mock('../api/client', () => ({
  default: {
    GET: (...args: unknown[]) => mockGet(...args),
    PATCH: (...args: unknown[]) => mockPatch(...args),
    DELETE: (...args: unknown[]) => mockDelete(...args),
  },
  ApiError: class ApiError extends Error {
    status: number
    body: unknown
    constructor(status: number, body: unknown) {
      super(`API error ${status}`)
      this.name = 'ApiError'
      this.status = status
      this.body = body
    }
  },
  registerLoginRedirect: vi.fn(),
}))

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function renderRecords() {
  return renderWithProviders(<RecordsPage />, { initialPath: '/records' })
}

/** Make GET mock respond based on path. */
function setupGetMock({
  pooItems = [POO_RECORD],
  locationItems = [LOCATION_RECORD],
  pooOffset = 0,
  locationOffset = 0,
}: {
  pooItems?: typeof POO_RECORD[]
  locationItems?: typeof LOCATION_RECORD[]
  pooOffset?: number
  locationOffset?: number
} = {}) {
  mockGet.mockImplementation((path: string, opts?: { params?: { query?: { offset?: number } } }) => {
    const offset = opts?.params?.query?.offset ?? 0
    if (path === '/api/poo') {
      return Promise.resolve({
        data: { items: pooItems, limit: 100, offset: pooOffset || offset },
        response: { status: 200, ok: true },
      })
    }
    if (path === '/api/locations') {
      return Promise.resolve({
        data: { items: locationItems, limit: 100, offset: locationOffset || offset },
        response: { status: 200, ok: true },
      })
    }
    return Promise.resolve({ data: null })
  })
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe('RecordsPage — Poo tab', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    setupGetMock()
  })

  // -------------------------------------------------------------------------
  // 1. Poo list renders
  // -------------------------------------------------------------------------

  it('renders poo records from GET /api/poo', async () => {
    renderRecords()

    await waitFor(() => {
      expect(screen.getByTestId('poo-table')).toBeInTheDocument()
    })

    expect(screen.getByText('2026-06-12T10:00:00Z')).toBeInTheDocument()
    expect(screen.getByText('done')).toBeInTheDocument()
  })

  // -------------------------------------------------------------------------
  // 2. Poo pagination: page 2 sends offset=100
  // -------------------------------------------------------------------------

  it('requests offset=100 when page 2 is selected', async () => {
    // Return full page to trigger pagination display.
    const page1 = makePooPage(0)
    setupGetMock({ pooItems: page1 })

    renderRecords()

    await waitFor(() => {
      expect(screen.getByTestId('poo-pagination')).toBeInTheDocument()
    })

    // Click page 2.
    const page2Button = screen.getByRole('button', { name: '2' })
    fireEvent.click(page2Button)

    await waitFor(() => {
      const allCalls = mockGet.mock.calls.filter((c) => c[0] === '/api/poo')
      const page2Call = allCalls.find(
        (c) => (c[1]?.params?.query?.offset ?? 0) === 100,
      )
      expect(page2Call).toBeDefined()
    })
  })

  // -------------------------------------------------------------------------
  // 3. Edit poo: opens modal, submit calls PATCH with encoded PK
  // -------------------------------------------------------------------------

  it('opens EditPooModal when Edit is clicked; submit calls PATCH with raw PK in path params and correct body', async () => {
    mockPatch.mockResolvedValue({ data: POO_RECORD, response: { status: 200, ok: true } })
    renderRecords()

    await waitFor(() => {
      expect(screen.getByTestId(`poo-edit-${POO_RECORD.timestamp}`)).toBeInTheDocument()
    })

    fireEvent.click(screen.getByTestId(`poo-edit-${POO_RECORD.timestamp}`))

    // Modal appears
    await waitFor(() => {
      expect(screen.getByTestId('edit-poo-modal')).toBeInTheDocument()
    })

    // Change status
    const statusInput = screen.getByTestId('poo-status-input') as HTMLInputElement
    fireEvent.change(statusInput, { target: { value: 'reviewed' } })

    // Submit
    fireEvent.submit(screen.getByTestId('edit-poo-form'))

    await waitFor(() => {
      expect(mockPatch).toHaveBeenCalled()
    })

    const patchCall = mockPatch.mock.calls[0]
    expect(patchCall[0]).toBe('/api/poo/{timestamp}')
    // PK must be the raw value — openapi-fetch encodes it once; hooks must not pre-encode.
    expect(patchCall[1].params.path.timestamp).toBe(POO_RECORD.timestamp)
    // Body must include only non-PK fields
    expect(patchCall[1].body).toHaveProperty('status')
    expect(patchCall[1].body).not.toHaveProperty('timestamp')
  })

  // -------------------------------------------------------------------------
  // 4. Delete poo: confirmation then DELETE called; list refreshes
  // -------------------------------------------------------------------------

  it('shows confirmation modal on Delete click; DELETE is called after confirmation', async () => {
    mockDelete.mockResolvedValue({ data: null, response: { status: 204, ok: true } })
    renderRecords()

    await waitFor(() => {
      expect(screen.getByTestId(`poo-delete-${POO_RECORD.timestamp}`)).toBeInTheDocument()
    })

    // Click Delete — confirmation modal appears
    fireEvent.click(screen.getByTestId(`poo-delete-${POO_RECORD.timestamp}`))

    await waitFor(() => {
      expect(screen.getByTestId('confirm-delete-modal')).toBeInTheDocument()
    })

    // The modal should show a helpful message
    expect(screen.getByTestId('confirm-delete-message')).toBeInTheDocument()

    // Cancel first — modal should close
    fireEvent.click(screen.getByTestId('confirm-delete-cancel'))

    await waitFor(() => {
      expect(screen.queryByTestId('confirm-delete-modal')).not.toBeInTheDocument()
    })

    // Reopen and confirm
    fireEvent.click(screen.getByTestId(`poo-delete-${POO_RECORD.timestamp}`))

    await waitFor(() => {
      expect(screen.getByTestId('confirm-delete-confirm')).toBeInTheDocument()
    })

    fireEvent.click(screen.getByTestId('confirm-delete-confirm'))

    await waitFor(() => {
      expect(mockDelete).toHaveBeenCalled()
    })

    const deleteCall = mockDelete.mock.calls[0]
    expect(deleteCall[0]).toBe('/api/poo/{timestamp}')
    // PK must be the raw value — hooks must not pre-encode; openapi-fetch encodes once.
    expect(deleteCall[1].params.path.timestamp).toBe(POO_RECORD.timestamp)
  })
})

describe('RecordsPage — Locations tab', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    setupGetMock()
  })

  // -------------------------------------------------------------------------
  // 5. Location list renders
  // -------------------------------------------------------------------------

  it('renders location records after switching to Locations tab', async () => {
    renderRecords()

    // Switch to Locations tab
    await waitFor(() => {
      expect(screen.getByTestId('tab-locations')).toBeInTheDocument()
    })

    fireEvent.click(screen.getByTestId('tab-locations'))

    await waitFor(() => {
      expect(screen.getByTestId('location-table')).toBeInTheDocument()
    })

    expect(screen.getByText('alice')).toBeInTheDocument()
    expect(screen.getByText('2026-06-12T09:00:00Z')).toBeInTheDocument()
  })

  // -------------------------------------------------------------------------
  // 6. Location pagination: page 2 sends offset=100
  // -------------------------------------------------------------------------

  it('requests offset=100 for locations when page 2 is selected', async () => {
    const page1 = makeLocationPage(0)
    setupGetMock({ locationItems: page1 })

    renderRecords()

    // Switch to Locations tab
    await waitFor(() => {
      expect(screen.getByTestId('tab-locations')).toBeInTheDocument()
    })
    fireEvent.click(screen.getByTestId('tab-locations'))

    await waitFor(() => {
      expect(screen.getByTestId('location-pagination')).toBeInTheDocument()
    })

    const page2Button = screen.getByRole('button', { name: '2' })
    fireEvent.click(page2Button)

    await waitFor(() => {
      const allCalls = mockGet.mock.calls.filter((c) => c[0] === '/api/locations')
      const page2Call = allCalls.find(
        (c) => (c[1]?.params?.query?.offset ?? 0) === 100,
      )
      expect(page2Call).toBeDefined()
    })
  })

  // -------------------------------------------------------------------------
  // 7. Edit location: opens modal, submit calls PATCH with encoded PK
  // -------------------------------------------------------------------------

  it('opens EditLocationModal; submit calls PATCH with raw person+datetime in path params', async () => {
    mockPatch.mockResolvedValue({ data: LOCATION_RECORD, response: { status: 200, ok: true } })

    renderRecords()

    fireEvent.click(screen.getByTestId('tab-locations'))

    const rowKey = `${LOCATION_RECORD.person}__${LOCATION_RECORD.datetime}`

    await waitFor(() => {
      expect(screen.getByTestId(`location-edit-${rowKey}`)).toBeInTheDocument()
    })

    fireEvent.click(screen.getByTestId(`location-edit-${rowKey}`))

    await waitFor(() => {
      expect(screen.getByTestId('edit-location-modal')).toBeInTheDocument()
    })

    // PK shown read-only in the modal (may appear more than once in the page: table + modal)
    const modalEl = screen.getByTestId('edit-location-modal')
    expect(modalEl).toBeInTheDocument()
    // 'alice' and datetime appear in modal read-only text
    expect(modalEl.textContent).toContain('alice')
    expect(modalEl.textContent).toContain('2026-06-12T09:00:00Z')

    // Submit
    fireEvent.submit(screen.getByTestId('edit-location-form'))

    await waitFor(() => {
      expect(mockPatch).toHaveBeenCalled()
    })

    const patchCall = mockPatch.mock.calls[0]
    expect(patchCall[0]).toBe('/api/locations/{person}/{datetime}')
    // PKs must be raw — hooks must not pre-encode; openapi-fetch encodes once.
    expect(patchCall[1].params.path.person).toBe(LOCATION_RECORD.person)
    expect(patchCall[1].params.path.datetime).toBe(LOCATION_RECORD.datetime)
    // Body must NOT contain PK fields
    expect(patchCall[1].body).not.toHaveProperty('person')
    expect(patchCall[1].body).not.toHaveProperty('datetime')
    expect(patchCall[1].body).toHaveProperty('latitude')
    expect(patchCall[1].body).toHaveProperty('longitude')
  })

  // -------------------------------------------------------------------------
  // 8. Delete location: confirmation then DELETE called
  // -------------------------------------------------------------------------

  it('shows confirmation modal on Delete; DELETE is called with raw PK params', async () => {
    mockDelete.mockResolvedValue({ data: null, response: { status: 204, ok: true } })

    renderRecords()
    fireEvent.click(screen.getByTestId('tab-locations'))

    const rowKey = `${LOCATION_RECORD.person}__${LOCATION_RECORD.datetime}`

    await waitFor(() => {
      expect(screen.getByTestId(`location-delete-${rowKey}`)).toBeInTheDocument()
    })

    fireEvent.click(screen.getByTestId(`location-delete-${rowKey}`))

    await waitFor(() => {
      expect(screen.getByTestId('confirm-delete-modal')).toBeInTheDocument()
    })

    fireEvent.click(screen.getByTestId('confirm-delete-confirm'))

    await waitFor(() => {
      expect(mockDelete).toHaveBeenCalled()
    })

    const deleteCall = mockDelete.mock.calls[0]
    expect(deleteCall[0]).toBe('/api/locations/{person}/{datetime}')
    // PKs must be raw — hooks must not pre-encode; openapi-fetch encodes once.
    expect(deleteCall[1].params.path.person).toBe(LOCATION_RECORD.person)
    expect(deleteCall[1].params.path.datetime).toBe(LOCATION_RECORD.datetime)
  })
})

// ---------------------------------------------------------------------------
// Additional: multiple poo records with correct timestamps
// ---------------------------------------------------------------------------

describe('RecordsPage — multiple poo rows', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    setupGetMock({ pooItems: [POO_RECORD, POO_RECORD_2] })
  })

  it('renders both rows', async () => {
    renderRecords()

    await waitFor(() => {
      expect(screen.getByTestId('poo-table')).toBeInTheDocument()
    })

    expect(screen.getByText('2026-06-12T10:00:00Z')).toBeInTheDocument()
    expect(screen.getByText('2026-06-12T11:00:00Z')).toBeInTheDocument()
  })
})
