/**
 * Tests for MeterManager component.
 *
 * Coverage:
 * 1. Loading state rendering.
 * 2. Error state rendering.
 * 3. Empty state when no meters exist.
 * 4. Meter timeline list rendering (label, dates, active badge, reason).
 * 5. "Declare New Meter" button opens form modal.
 * 6. Declare meter — form submit calls POST /api/energy/meters.
 * 7. Declare meter — 422 (倒挂) error is displayed.
 * 8. Edit button opens edit form modal.
 * 9. Edit meter — saves label/note via PATCH /api/energy/meters/{meter_id}.
 * 10. Edit meter — retroactive started_at triggers recompute notice.
 */

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { renderWithProviders } from '../test-utils'
import { MeterManager } from './MeterManager'

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

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

vi.mock('../api/client', () => ({
  default: {
    GET: (...args: unknown[]) => mockGet(...args),
    POST: (...args: unknown[]) => mockPost(...args),
    PATCH: (...args: unknown[]) => mockPatch(...args),
    DELETE: vi.fn(),
  },
  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(),
}))

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

const ACTIVE_METER = {
  id: 1,
  label: 'Initial 2G meter',
  commodity: 'electricity',
  started_at: '2024-01-15T00:00:00Z',
  ended_at: null,
  reason: 'initial',
  note: null,
  created_at: '2024-01-15T00:00:00Z',
}

const CLOSED_METER = {
  id: 2,
  label: 'Old 4G meter',
  commodity: 'electricity',
  started_at: '2023-06-01T00:00:00Z',
  ended_at: '2024-01-15T00:00:00Z',
  reason: 'meter_swap',
  note: 'Replaced by grid company',
  created_at: '2023-06-01T00:00:00Z',
}

const METERS_RESPONSE = {
  items: [CLOSED_METER, ACTIVE_METER],
  total: 2,
}

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

describe('MeterManager — loading / error / empty states', () => {
  beforeEach(() => vi.clearAllMocks())

  it('renders loading state initially', () => {
    mockGet.mockImplementation(() => new Promise(() => {}))

    renderWithProviders(<MeterManager />)

    expect(screen.getByTestId('meters-loading')).toBeInTheDocument()
  })

  it('renders error state when GET fails', async () => {
    mockGet.mockRejectedValue(new Error('Network error'))

    renderWithProviders(<MeterManager />)

    await waitFor(() => {
      expect(screen.getByTestId('meters-load-error')).toBeInTheDocument()
    })
  })

  it('renders empty state when no meters exist', async () => {
    mockGet.mockResolvedValue({ data: { items: [], total: 0 } })

    renderWithProviders(<MeterManager />)

    await waitFor(() => {
      expect(screen.getByTestId('meters-empty')).toBeInTheDocument()
    })
  })
})

describe('MeterManager — meter list', () => {
  beforeEach(() => vi.clearAllMocks())

  it('renders meter timeline with label, dates, status badge, reason', async () => {
    mockGet.mockResolvedValue({ data: METERS_RESPONSE })

    renderWithProviders(<MeterManager />)

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

    // Labels
    expect(screen.getByText('Initial 2G meter')).toBeInTheDocument()
    expect(screen.getByText('Old 4G meter')).toBeInTheDocument()

    // Status badges
    expect(screen.getByTestId(`meter-status-${ACTIVE_METER.id}`)).toHaveTextContent('active')
    expect(screen.getByTestId(`meter-status-${CLOSED_METER.id}`)).toHaveTextContent('closed')

    // Reason badges
    expect(screen.getByText('initial')).toBeInTheDocument()
    expect(screen.getByText('meter_swap')).toBeInTheDocument()
  })

  it('renders "Declare New Meter" button', async () => {
    mockGet.mockResolvedValue({ data: METERS_RESPONSE })

    renderWithProviders(<MeterManager />)

    await waitFor(() => {
      expect(screen.getByTestId('meter-declare-button')).toBeInTheDocument()
    })
  })
})

describe('MeterManager — declare new meter', () => {
  beforeEach(() => vi.clearAllMocks())

  it('opens declare modal when "Declare New Meter" is clicked', async () => {
    const user = userEvent.setup()
    mockGet.mockResolvedValue({ data: { items: [], total: 0 } })

    renderWithProviders(<MeterManager />)

    await waitFor(() => expect(screen.getByTestId('meter-declare-button')).toBeInTheDocument())

    await user.click(screen.getByTestId('meter-declare-button'))

    await waitFor(() => {
      expect(screen.getByTestId('declare-meter-modal')).toBeInTheDocument()
    })
    expect(screen.getByTestId('declare-meter-form')).toBeInTheDocument()
  })

  it('calls POST /api/energy/meters with correct payload including local-midnight naive datetime', async () => {
    const user = userEvent.setup()
    mockGet.mockResolvedValue({ data: { items: [], total: 0 } })
    mockPost.mockResolvedValue({ data: ACTIVE_METER })

    renderWithProviders(<MeterManager />)

    await waitFor(() => expect(screen.getByTestId('meter-declare-button')).toBeInTheDocument())
    await user.click(screen.getByTestId('meter-declare-button'))

    await waitFor(() => expect(screen.getByTestId('declare-meter-form')).toBeInTheDocument())

    // Fill form
    await user.type(screen.getByTestId('meter-label'), 'New meter label')
    await user.type(screen.getByTestId('meter-started-at'), '2026-01-01')

    // Select reason via the combobox (Mantine Select renders a combobox)
    await user.click(screen.getByTestId('meter-reason'))
    await waitFor(() => screen.getByText('Initial installation'))
    await user.click(screen.getByText('Initial installation'))

    await user.click(screen.getByTestId('declare-meter-submit'))

    await waitFor(() => {
      expect(mockPost).toHaveBeenCalledWith(
        '/api/energy/meters',
        expect.objectContaining({
          body: expect.objectContaining({
            label: 'New meter label',
            // FU10 local-midnight naive convention: no Z suffix
            started_at: '2026-01-01T00:00:00',
            reason: 'initial',
            commodity: 'electricity',
          }),
        }),
      )
    })
  })

  it('displays error when POST fails with 422 (倒挂 / validation error)', async () => {
    const user = userEvent.setup()
    mockGet.mockResolvedValue({ data: { items: [ACTIVE_METER], total: 1 } })

    const { ApiError } = await import('../api/client')
    mockPost.mockRejectedValue(
      new ApiError(422, { detail: 'started_at must be ≥ current active meter started_at' }),
    )

    renderWithProviders(<MeterManager />)

    await waitFor(() => expect(screen.getByTestId('meter-declare-button')).toBeInTheDocument())
    await user.click(screen.getByTestId('meter-declare-button'))
    await waitFor(() => expect(screen.getByTestId('declare-meter-form')).toBeInTheDocument())

    await user.type(screen.getByTestId('meter-label'), 'Bad meter')
    await user.type(screen.getByTestId('meter-started-at'), '2020-01-01')
    await user.click(screen.getByTestId('meter-reason'))
    await waitFor(() => screen.getByText('Meter swap (same address)'))
    await user.click(screen.getByText('Meter swap (same address)'))

    await user.click(screen.getByTestId('declare-meter-submit'))

    await waitFor(() => {
      expect(screen.getByTestId('declare-meter-error')).toBeInTheDocument()
    })
    expect(screen.getByTestId('declare-meter-error').textContent).toContain(
      'started_at must be ≥',
    )
  })

  it('cancel button closes the declare modal', async () => {
    const user = userEvent.setup()
    mockGet.mockResolvedValue({ data: { items: [], total: 0 } })

    renderWithProviders(<MeterManager />)

    await waitFor(() => expect(screen.getByTestId('meter-declare-button')).toBeInTheDocument())
    await user.click(screen.getByTestId('meter-declare-button'))
    await waitFor(() => expect(screen.getByTestId('declare-meter-modal')).toBeInTheDocument())

    await user.click(screen.getByTestId('declare-meter-cancel'))

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

describe('MeterManager — edit meter date initialisation', () => {
  beforeEach(() => vi.clearAllMocks())

  it('initialises date input from started_at (Z-suffix, UTC midnight → local date)', async () => {
    // ACTIVE_METER.started_at = '2024-01-15T00:00:00Z' (UTC midnight).
    // The test suite is pinned to TZ=UTC (via vite.config.ts test.env), so
    // the local date is deterministically '2024-01-15' on any CI runner.
    const user = userEvent.setup()
    mockGet.mockResolvedValue({ data: { items: [ACTIVE_METER], total: 1 } })

    renderWithProviders(<MeterManager />)

    await waitFor(() => expect(screen.getByTestId(`meter-edit-${ACTIVE_METER.id}`)).toBeInTheDocument())
    await user.click(screen.getByTestId(`meter-edit-${ACTIVE_METER.id}`))
    await waitFor(() => expect(screen.getByTestId('edit-meter-form')).toBeInTheDocument())

    const dateInput = screen.getByTestId('edit-meter-started-at') as HTMLInputElement
    expect(dateInput.value).toBe('2024-01-15')
  })

  it('initialises date input from started_at (naive, no tz marker)', async () => {
    // A naive timestamp without timezone marker — parseBackendTimestamp appends 'Z'
    // so it is treated as UTC. With TZ=UTC (pinned in vite.config.ts), the local date
    // equals the UTC date exactly.
    const naiveMeter = { ...ACTIVE_METER, started_at: '2024-03-20T00:00:00' }
    const user = userEvent.setup()
    mockGet.mockResolvedValue({ data: { items: [naiveMeter], total: 1 } })

    renderWithProviders(<MeterManager />)

    await waitFor(() => expect(screen.getByTestId(`meter-edit-${naiveMeter.id}`)).toBeInTheDocument())
    await user.click(screen.getByTestId(`meter-edit-${naiveMeter.id}`))
    await waitFor(() => expect(screen.getByTestId('edit-meter-form')).toBeInTheDocument())

    const dateInput = screen.getByTestId('edit-meter-started-at') as HTMLInputElement
    expect(dateInput.value).toBe('2024-03-20')
  })

  it('initialises date input from started_at with explicit UTC offset (+02:00) — regression for old buggy regex', async () => {
    // The old hand-written regex /[zZ+-]\d*$/ would fail to match '+02:00' (the ':00'
    // suffix broke the pattern) and would incorrectly append 'Z', producing an Invalid Date.
    // The new code uses parseBackendTimestamp which uses the correct TZ_MARKER_RE regex
    // and handles explicit offsets properly.
    const offsetMeter = { ...ACTIVE_METER, started_at: '2024-01-15T02:00:00+02:00' }
    // UTC equivalent: 2024-01-15T00:00:00Z → with TZ=UTC (pinned) local date = '2024-01-15'
    const user = userEvent.setup()
    mockGet.mockResolvedValue({ data: { items: [offsetMeter], total: 1 } })

    renderWithProviders(<MeterManager />)

    await waitFor(() => expect(screen.getByTestId(`meter-edit-${offsetMeter.id}`)).toBeInTheDocument())
    await user.click(screen.getByTestId(`meter-edit-${offsetMeter.id}`))
    await waitFor(() => expect(screen.getByTestId('edit-meter-form')).toBeInTheDocument())

    const dateInput = screen.getByTestId('edit-meter-started-at') as HTMLInputElement
    // Must not be empty (which would indicate Invalid Date from the old buggy path)
    expect(dateInput.value).not.toBe('')
    expect(dateInput.value).toBe('2024-01-15')
  })

  it('does not include started_at in PATCH body when date is unchanged (round-trip idempotence)', async () => {
    // Open the edit form and immediately submit without changing any fields except label.
    // The date should be considered unchanged → no started_at in the PATCH body.
    const user = userEvent.setup()
    mockGet.mockResolvedValue({ data: { items: [ACTIVE_METER], total: 1 } })
    mockPatch.mockResolvedValue({ data: { ...ACTIVE_METER, label: 'New label' } })

    renderWithProviders(<MeterManager />)

    await waitFor(() => expect(screen.getByTestId(`meter-edit-${ACTIVE_METER.id}`)).toBeInTheDocument())
    await user.click(screen.getByTestId(`meter-edit-${ACTIVE_METER.id}`))
    await waitFor(() => expect(screen.getByTestId('edit-meter-form')).toBeInTheDocument())

    // Change only label; leave date untouched
    const labelInput = screen.getByTestId('edit-meter-label')
    await user.clear(labelInput)
    await user.type(labelInput, 'New label')

    await user.click(screen.getByTestId('edit-meter-submit'))

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

    const patchBody = mockPatch.mock.calls[0][1].body
    expect(patchBody).not.toHaveProperty('started_at')
  })
})

describe('MeterManager — edit meter', () => {
  beforeEach(() => vi.clearAllMocks())

  it('opens edit modal when Edit button is clicked', async () => {
    const user = userEvent.setup()
    mockGet.mockResolvedValue({ data: METERS_RESPONSE })

    renderWithProviders(<MeterManager />)

    await waitFor(() => expect(screen.getByTestId(`meter-edit-${ACTIVE_METER.id}`)).toBeInTheDocument())

    await user.click(screen.getByTestId(`meter-edit-${ACTIVE_METER.id}`))

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

  it('calls PATCH /api/energy/meters/{meter_id} when label is changed', async () => {
    const user = userEvent.setup()
    mockGet.mockResolvedValue({ data: { items: [ACTIVE_METER], total: 1 } })
    mockPatch.mockResolvedValue({ data: { ...ACTIVE_METER, label: 'Renamed meter' } })

    renderWithProviders(<MeterManager />)

    await waitFor(() => expect(screen.getByTestId(`meter-edit-${ACTIVE_METER.id}`)).toBeInTheDocument())
    await user.click(screen.getByTestId(`meter-edit-${ACTIVE_METER.id}`))
    await waitFor(() => expect(screen.getByTestId('edit-meter-form')).toBeInTheDocument())

    // Clear label and type new one
    const labelInput = screen.getByTestId('edit-meter-label')
    await user.clear(labelInput)
    await user.type(labelInput, 'Renamed meter')

    await user.click(screen.getByTestId('edit-meter-submit'))

    await waitFor(() => {
      expect(mockPatch).toHaveBeenCalledWith(
        '/api/energy/meters/{meter_id}',
        expect.objectContaining({
          params: { path: { meter_id: ACTIVE_METER.id } },
          body: expect.objectContaining({ label: 'Renamed meter' }),
        }),
      )
    })
  })

  it('shows recompute notice when started_at is changed (retroactive correction)', async () => {
    const user = userEvent.setup()
    mockGet.mockResolvedValue({ data: { items: [ACTIVE_METER], total: 1 } })
    mockPatch.mockResolvedValue({ data: { ...ACTIVE_METER, started_at: '2024-02-01T00:00:00' } })

    renderWithProviders(<MeterManager />)

    await waitFor(() => expect(screen.getByTestId(`meter-edit-${ACTIVE_METER.id}`)).toBeInTheDocument())
    await user.click(screen.getByTestId(`meter-edit-${ACTIVE_METER.id}`))
    await waitFor(() => expect(screen.getByTestId('edit-meter-form')).toBeInTheDocument())

    // Change the date field
    const dateInput = screen.getByTestId('edit-meter-started-at')
    await user.clear(dateInput)
    await user.type(dateInput, '2024-02-01')

    await user.click(screen.getByTestId('edit-meter-submit'))

    await waitFor(() => {
      expect(screen.getByTestId('meter-recompute-notice')).toBeInTheDocument()
    })
  })

  it('displays error when PATCH fails', async () => {
    const user = userEvent.setup()
    mockGet.mockResolvedValue({ data: { items: [ACTIVE_METER], total: 1 } })

    const { ApiError } = await import('../api/client')
    mockPatch.mockRejectedValue(new ApiError(422, { detail: 'started_at conflict' }))

    renderWithProviders(<MeterManager />)

    await waitFor(() => expect(screen.getByTestId(`meter-edit-${ACTIVE_METER.id}`)).toBeInTheDocument())
    await user.click(screen.getByTestId(`meter-edit-${ACTIVE_METER.id}`))
    await waitFor(() => expect(screen.getByTestId('edit-meter-form')).toBeInTheDocument())

    // Change label so there's something to patch
    const labelInput = screen.getByTestId('edit-meter-label')
    await user.clear(labelInput)
    await user.type(labelInput, 'Different label')

    await user.click(screen.getByTestId('edit-meter-submit'))

    await waitFor(() => {
      expect(screen.getByTestId('edit-meter-error')).toBeInTheDocument()
    })
    expect(screen.getByTestId('edit-meter-error').textContent).toContain('started_at conflict')
  })
})
