/**
 * Tests for EnergyPage (M5-T06 + M5-polish).
 *
 * Coverage:
 * 1. Renders device list from GET /api/modbus/devices.
 * 2. Empty state when no devices.
 * 3. "New Device" button opens DeviceForm modal.
 * 4. Edit button opens DeviceForm modal pre-filled with device data.
 * 5. Delete button opens confirmation modal; confirm calls DELETE.
 * 6. Delete 409 error shows friendly hint in confirmation modal.
 * 7. Test-read button calls POST /api/modbus/devices/{uuid}/test and shows result.
 * 8. Test-read failure shows error in modal.
 * 9. Auto-refresh switch is rendered (default on) and can be toggled.
 */

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

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

const DEVICE = {
  uuid: 'abc-123',
  friendly_name: 'SDM120 Main',
  transport: 'tcp',
  host: '192.168.1.100',
  port: 502,
  unit_id: 1,
  profile: 'sdm120',
  poll_interval_s: 5,
  enabled: true,
  last_poll_at: '2026-06-22T10:00:00Z',
  last_poll_ok: true,
  created_at: '2026-06-01T00:00:00Z',
  updated_at: '2026-06-01T00:00:00Z',
}

const PROFILES_RESP = {
  profiles: [{ name: 'sdm120', description: 'Eastron SDM120 single-phase energy meter' }],
}

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

const mockGet = vi.fn()
const mockPost = vi.fn()
const mockDelete = 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: (...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 setupDefaultMocks(devices = [DEVICE]) {
  mockGet.mockImplementation((path: string) => {
    if (path === '/api/modbus/devices') {
      return Promise.resolve({ data: { items: devices, total: devices.length } })
    }
    if (path === '/api/modbus/profiles') {
      return Promise.resolve({ data: PROFILES_RESP })
    }
    return Promise.resolve({ data: null })
  })
}

function renderEnergy() {
  return renderWithProviders(<EnergyPage />, { initialPath: '/energy' })
}

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

describe('EnergyPage — device list', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    setupDefaultMocks()
  })

  it('renders device list from GET /api/modbus/devices', async () => {
    renderEnergy()

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

    // Device name appears in multiple places now (table row + readings section + chart title).
    const nameElements = screen.getAllByText('SDM120 Main')
    expect(nameElements.length).toBeGreaterThan(0)
    expect(screen.getByText('192.168.1.100')).toBeInTheDocument()
    // 'sdm120' may also appear in multiple places (table badge + chart).
    const profileElements = screen.getAllByText('sdm120')
    expect(profileElements.length).toBeGreaterThan(0)
  })

  it('shows empty state when no devices', async () => {
    setupDefaultMocks([])
    renderEnergy()

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

  it('shows loading state initially', () => {
    // Delay resolution so we can catch the loading state
    mockGet.mockReturnValue(new Promise(() => {}))
    renderEnergy()

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

describe('EnergyPage — create device', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    setupDefaultMocks()
  })

  it('opens DeviceForm modal when New Device is clicked', async () => {
    renderEnergy()

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

    fireEvent.click(screen.getByTestId('device-new-button'))

    // Wait for both modal AND the form (which appears after profiles load)
    await waitFor(() => {
      expect(screen.getByTestId('device-form-modal')).toBeInTheDocument()
    })

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

    expect(screen.getByTestId('device-friendly-name')).toBeInTheDocument()
    expect(screen.getByTestId('device-host')).toBeInTheDocument()
  })

  it('submit on new device form calls POST /api/modbus/devices', async () => {
    mockPost.mockResolvedValue({ data: DEVICE })
    renderEnergy()

    await waitFor(() => expect(screen.getByTestId('device-new-button')).toBeInTheDocument())
    fireEvent.click(screen.getByTestId('device-new-button'))

    // Wait for profiles to load so the form is visible (profiles auto-select sdm120)
    await waitFor(() => expect(screen.getByTestId('device-form')).toBeInTheDocument())

    // Fill required fields (profile is auto-selected to 'sdm120')
    fireEvent.change(screen.getByTestId('device-friendly-name'), {
      target: { value: 'Test Device' },
    })
    fireEvent.change(screen.getByTestId('device-host'), {
      target: { value: '10.0.0.1' },
    })

    fireEvent.submit(screen.getByTestId('device-form'))

    await waitFor(() => {
      expect(mockPost).toHaveBeenCalledWith(
        '/api/modbus/devices',
        expect.objectContaining({
          body: expect.objectContaining({
            friendly_name: 'Test Device',
            host: '10.0.0.1',
            profile: 'sdm120',
          }),
        }),
      )
    })
  })
})

describe('EnergyPage — edit device', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    setupDefaultMocks()
  })

  it('opens DeviceForm modal with device data when Edit is clicked', async () => {
    renderEnergy()

    await waitFor(() => {
      expect(screen.getByTestId(`device-edit-${DEVICE.uuid}`)).toBeInTheDocument()
    })

    fireEvent.click(screen.getByTestId(`device-edit-${DEVICE.uuid}`))

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

    // Wait for profiles to load so form body is visible
    await waitFor(() => {
      expect(screen.getByTestId('device-form')).toBeInTheDocument()
    })

    // UUID shown in edit mode
    expect(screen.getByTestId('device-uuid-display').textContent).toContain(DEVICE.uuid)
  })
})

describe('EnergyPage — delete device', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    setupDefaultMocks()
  })

  it('shows confirmation modal on Delete click', async () => {
    renderEnergy()

    await waitFor(() => {
      expect(screen.getByTestId(`device-delete-${DEVICE.uuid}`)).toBeInTheDocument()
    })

    fireEvent.click(screen.getByTestId(`device-delete-${DEVICE.uuid}`))

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

    expect(screen.getByTestId('device-delete-message').textContent).toContain('SDM120 Main')
  })

  it('calls DELETE on confirm and closes modal', async () => {
    mockDelete.mockResolvedValue({ data: null })
    renderEnergy()

    await waitFor(() => expect(screen.getByTestId(`device-delete-${DEVICE.uuid}`)).toBeInTheDocument())
    fireEvent.click(screen.getByTestId(`device-delete-${DEVICE.uuid}`))
    await waitFor(() => expect(screen.getByTestId('device-delete-confirm')).toBeInTheDocument())

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

    await waitFor(() => {
      expect(mockDelete).toHaveBeenCalledWith('/api/modbus/devices/{uuid}', {
        params: { path: { uuid: DEVICE.uuid } },
      })
    })
  })

  it('cancel button closes confirmation modal', async () => {
    renderEnergy()

    await waitFor(() => expect(screen.getByTestId(`device-delete-${DEVICE.uuid}`)).toBeInTheDocument())
    fireEvent.click(screen.getByTestId(`device-delete-${DEVICE.uuid}`))
    await waitFor(() => expect(screen.getByTestId('device-delete-cancel')).toBeInTheDocument())

    fireEvent.click(screen.getByTestId('device-delete-cancel'))

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

  it('shows friendly 409 hint when device has readings', async () => {
    const { ApiError } = await import('../api/client')
    mockDelete.mockRejectedValue(new ApiError(409, { detail: 'device has readings' }))

    renderEnergy()

    await waitFor(() => expect(screen.getByTestId(`device-delete-${DEVICE.uuid}`)).toBeInTheDocument())
    fireEvent.click(screen.getByTestId(`device-delete-${DEVICE.uuid}`))
    // On first open (no 409 yet), the regular confirm button is visible.
    await waitFor(() => expect(screen.getByTestId('device-delete-confirm')).toBeInTheDocument())

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

    await waitFor(() => {
      expect(screen.getByTestId('device-delete-409-hint')).toBeInTheDocument()
    })
    // After 409, the force-delete button appears instead of the normal confirm.
    expect(screen.getByTestId('device-delete-force')).toBeInTheDocument()
    expect(screen.queryByTestId('device-delete-confirm')).not.toBeInTheDocument()
  })

  it('force-delete button triggers cascade delete after 409', async () => {
    const { ApiError } = await import('../api/client')
    // First call → 409, second call (cascade) → success
    mockDelete
      .mockRejectedValueOnce(new ApiError(409, { detail: 'device has readings' }))
      .mockResolvedValueOnce({ data: { deleted: true, readings_deleted: 3, toggles_deleted: 1 } })

    renderEnergy()

    await waitFor(() => expect(screen.getByTestId(`device-delete-${DEVICE.uuid}`)).toBeInTheDocument())
    fireEvent.click(screen.getByTestId(`device-delete-${DEVICE.uuid}`))
    await waitFor(() => expect(screen.getByTestId('device-delete-confirm')).toBeInTheDocument())

    // First attempt → 409
    fireEvent.click(screen.getByTestId('device-delete-confirm'))

    // Force-delete button appears
    await waitFor(() => expect(screen.getByTestId('device-delete-force')).toBeInTheDocument())

    // Click force-delete → should call DELETE with cascade=true
    fireEvent.click(screen.getByTestId('device-delete-force'))

    await waitFor(() => {
      expect(mockDelete).toHaveBeenCalledWith('/api/modbus/devices/{uuid}', {
        params: { path: { uuid: DEVICE.uuid }, query: { cascade: true } },
      })
    })
  })
})

describe('EnergyPage — test-read', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    setupDefaultMocks()
  })

  it('shows success payload modal after successful test-read', async () => {
    mockPost.mockResolvedValue({
      data: { ok: true, payload: { voltage: 230.2, current: 1.3 } },
    })

    renderEnergy()

    await waitFor(() => {
      expect(screen.getByTestId(`device-test-${DEVICE.uuid}`)).toBeInTheDocument()
    })

    fireEvent.click(screen.getByTestId(`device-test-${DEVICE.uuid}`))

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

    expect(screen.getByTestId('test-read-ok')).toBeInTheDocument()
    // Values rendered via formatMetricValue — voltage 230.2 → "230.20"
    expect(screen.getByTestId('test-read-payload').textContent).toContain('230.2')
  })

  it('shows error modal after failed test-read', async () => {
    mockPost.mockResolvedValue({
      data: { ok: false, error: 'Connection timed out' },
    })

    renderEnergy()

    await waitFor(() => {
      expect(screen.getByTestId(`device-test-${DEVICE.uuid}`)).toBeInTheDocument()
    })

    fireEvent.click(screen.getByTestId(`device-test-${DEVICE.uuid}`))

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

    expect(screen.getByTestId('test-read-error')).toBeInTheDocument()
    expect(screen.getByTestId('test-read-error').textContent).toContain('Connection timed out')
  })
})

describe('EnergyPage — meters tab', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    setupDefaultMocks()
  })

  it('renders Meters tab in the tab list', async () => {
    renderEnergy()

    await waitFor(() => {
      expect(screen.getByTestId('tab-meters')).toBeInTheDocument()
    })
    expect(screen.getByTestId('tab-meters').textContent).toContain('Meters')
  })

  it('renders meters panel when Meters tab is clicked', async () => {
    mockGet.mockImplementation((path: string) => {
      if (path === '/api/modbus/devices') {
        return Promise.resolve({ data: { items: [DEVICE], total: 1 } })
      }
      if (path === '/api/energy/meters') {
        return Promise.resolve({ data: { items: [], total: 0 } })
      }
      return Promise.resolve({ data: null })
    })

    renderEnergy()

    await waitFor(() => expect(screen.getByTestId('tab-meters')).toBeInTheDocument())

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

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

describe('EnergyPage — auto-refresh switch', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    setupDefaultMocks()
  })

  it('renders auto-refresh switch when devices exist', async () => {
    renderEnergy()

    await waitFor(() => {
      expect(screen.getByTestId('auto-refresh-switch')).toBeInTheDocument()
    })
  })

  it('auto-refresh switch is checked by default (default on)', async () => {
    renderEnergy()

    await waitFor(() => {
      expect(screen.getByTestId('auto-refresh-switch')).toBeInTheDocument()
    })

    // Mantine Switch uses role="switch" and places data-testid directly on the
    // <input> element; query it by the testid or by role.
    const switchInput = screen.getByRole('switch', { name: /auto-refresh/i })
    expect(switchInput).toBeChecked()
  })

  it('toggling auto-refresh switch changes its checked state', async () => {
    renderEnergy()

    await waitFor(() => {
      expect(screen.getByTestId('auto-refresh-switch')).toBeInTheDocument()
    })

    const switchInput = screen.getByRole('switch', { name: /auto-refresh/i })
    expect(switchInput).toBeChecked()

    fireEvent.click(switchInput)

    await waitFor(() => {
      expect(switchInput).not.toBeChecked()
    })
  })
})
