/**
 * Tests for ExposeSettings (M5-T12).
 *
 * Strategy: vi.mock the apiClient so GET/PUT/POST responses are controlled
 * without a real server or MQTT broker.
 *
 * Coverage:
 * 1. Loading state shows a loader.
 * 2. Error state shows an error alert.
 * 3. Empty catalog shows the empty-state message.
 * 4. Catalog with entities: renders entity names, device groups, toggle switches.
 * 5. MQTT status badges render correctly (configured/connected/discovery).
 * 6. Toggling a switch calls PUT /api/expose with the correct key/bool.
 * 7. Re-publish button calls POST /api/expose/republish.
 * 8. Republish ok=true shows success alert; ok=false shows warning.
 * 9. value_getter is never rendered (no crash from non-serialisable callable).
 * 10. binary_sensor entities are shown in the catalog alongside sensors.
 */

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

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

const MOCK_MQTT_STATUS_OFF = {
  mqtt_configured: false,
  mqtt_connected: false,
  discovery_enabled: false,
}

const MOCK_MQTT_STATUS_ON = {
  mqtt_configured: true,
  mqtt_connected: true,
  discovery_enabled: true,
}

const MOCK_CATALOG_EMPTY = {
  catalog: [],
  mqtt_status: MOCK_MQTT_STATUS_OFF,
}

const MOCK_CATALOG_ONE_DEVICE = {
  catalog: [
    {
      entity: {
        key: 'modbus.abc.voltage',
        component: 'sensor',
        device: { identifiers: ['modbus', 'abc'], name: 'Test Meter' },
        device_class: 'voltage',
        unit: 'V',
        name: 'Test Meter Voltage',
        state_class: 'measurement',
      },
      enabled: false,
    },
    {
      entity: {
        key: 'modbus.abc.current',
        component: 'sensor',
        device: { identifiers: ['modbus', 'abc'], name: 'Test Meter' },
        device_class: 'current',
        unit: 'A',
        name: 'Test Meter Current',
        state_class: 'measurement',
      },
      enabled: true,
    },
    {
      entity: {
        key: 'modbus.abc.online',
        component: 'binary_sensor',
        device: { identifiers: ['modbus', 'abc'], name: 'Test Meter' },
        device_class: 'connectivity',
        unit: '',
        name: 'Test Meter Online',
        state_class: null,
      },
      enabled: false,
    },
  ],
  mqtt_status: MOCK_MQTT_STATUS_ON,
}

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

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

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

// ---------------------------------------------------------------------------
// Helper
// ---------------------------------------------------------------------------

function renderExpose() {
  return renderWithProviders(<ExposeSettings />, { initialPath: '/config' })
}

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

describe('ExposeSettings', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  // -------------------------------------------------------------------------
  // 1. Loading state
  // -------------------------------------------------------------------------

  it('shows a loader while the catalog is loading', () => {
    // Never resolves
    mockGet.mockReturnValue(new Promise(() => {}))
    renderExpose()
    expect(screen.getByTestId('expose-loading')).toBeInTheDocument()
  })

  // -------------------------------------------------------------------------
  // 2. Error state
  // -------------------------------------------------------------------------

  it('shows an error alert when the catalog request fails', async () => {
    mockGet.mockRejectedValue(new Error('Network error'))
    renderExpose()
    await waitFor(() => {
      expect(screen.getByTestId('expose-load-error')).toBeInTheDocument()
    })
  })

  // -------------------------------------------------------------------------
  // 3. Empty catalog
  // -------------------------------------------------------------------------

  it('shows empty-state message when catalog is empty', async () => {
    mockGet.mockResolvedValue({ data: MOCK_CATALOG_EMPTY })
    renderExpose()
    await waitFor(() => {
      expect(screen.getByTestId('expose-empty')).toBeInTheDocument()
    })
  })

  // -------------------------------------------------------------------------
  // 4. Catalog with entities
  // -------------------------------------------------------------------------

  it('renders entity names and device groups', async () => {
    mockGet.mockResolvedValue({ data: MOCK_CATALOG_ONE_DEVICE })
    renderExpose()

    await waitFor(() => {
      expect(screen.getByTestId('expose-settings')).toBeInTheDocument()
    })

    // Device group heading
    expect(screen.getByTestId('device-group-Test Meter')).toBeInTheDocument()

    // Entity names
    expect(screen.getByText('Test Meter Voltage')).toBeInTheDocument()
    expect(screen.getByText('Test Meter Current')).toBeInTheDocument()
    expect(screen.getByText('Test Meter Online')).toBeInTheDocument()
  })

  it('renders toggle switches for each entity', async () => {
    mockGet.mockResolvedValue({ data: MOCK_CATALOG_ONE_DEVICE })
    renderExpose()

    await waitFor(() => {
      expect(screen.getByTestId('entity-toggle-modbus.abc.voltage')).toBeInTheDocument()
    })

    // voltage is disabled (enabled=false in fixture)
    const voltageSwitch = screen.getByTestId(
      'entity-toggle-modbus.abc.voltage',
    ) as HTMLInputElement
    expect(voltageSwitch.checked).toBe(false)

    // current is enabled (enabled=true in fixture)
    const currentSwitch = screen.getByTestId(
      'entity-toggle-modbus.abc.current',
    ) as HTMLInputElement
    expect(currentSwitch.checked).toBe(true)
  })

  it('shows binary_sensor entities alongside sensor entities', async () => {
    mockGet.mockResolvedValue({ data: MOCK_CATALOG_ONE_DEVICE })
    renderExpose()

    await waitFor(() => {
      expect(screen.getByTestId('entity-row-modbus.abc.online')).toBeInTheDocument()
    })

    // binary_sensor row contains "binary_sensor" text
    const onlineRow = screen.getByTestId('entity-row-modbus.abc.online')
    expect(onlineRow).toHaveTextContent('binary_sensor')
  })

  // -------------------------------------------------------------------------
  // 5. MQTT status badges
  // -------------------------------------------------------------------------

  it('renders MQTT status badges', async () => {
    mockGet.mockResolvedValue({ data: MOCK_CATALOG_ONE_DEVICE })
    renderExpose()

    await waitFor(() => {
      expect(screen.getByTestId('mqtt-status-badges')).toBeInTheDocument()
    })

    expect(screen.getByTestId('badge-mqtt-configured')).toBeInTheDocument()
    expect(screen.getByTestId('badge-mqtt-connected')).toBeInTheDocument()
    expect(screen.getByTestId('badge-discovery-enabled')).toBeInTheDocument()
  })

  it('shows "Configured" when mqtt_configured is true', async () => {
    mockGet.mockResolvedValue({ data: MOCK_CATALOG_ONE_DEVICE })
    renderExpose()

    await waitFor(() => {
      expect(screen.getByTestId('badge-mqtt-configured')).toBeInTheDocument()
    })

    expect(screen.getByTestId('badge-mqtt-configured')).toHaveTextContent('MQTT Configured')
  })

  it('shows "Not Configured" when mqtt_configured is false', async () => {
    mockGet.mockResolvedValue({ data: MOCK_CATALOG_EMPTY })
    renderExpose()

    await waitFor(() => {
      expect(screen.getByTestId('badge-mqtt-configured')).toBeInTheDocument()
    })

    expect(screen.getByTestId('badge-mqtt-configured')).toHaveTextContent('MQTT Not Configured')
  })

  // -------------------------------------------------------------------------
  // 6. Toggle a switch → PUT /api/expose
  // -------------------------------------------------------------------------

  it('calls PUT /api/expose when a toggle is switched', async () => {
    mockGet.mockResolvedValue({ data: MOCK_CATALOG_ONE_DEVICE })
    mockPut.mockResolvedValue({ data: MOCK_CATALOG_ONE_DEVICE })

    renderExpose()

    await waitFor(() => {
      expect(screen.getByTestId('entity-toggle-modbus.abc.voltage')).toBeInTheDocument()
    })

    // Click the voltage toggle (currently off → turning on)
    const voltageSwitch = screen.getByTestId('entity-toggle-modbus.abc.voltage')
    fireEvent.click(voltageSwitch)

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

    const putCall = mockPut.mock.calls[0]
    // path arg
    expect(putCall[0]).toBe('/api/expose')
    // body
    expect(putCall[1]?.body?.toggles).toMatchObject({ 'modbus.abc.voltage': true })
  })

  // -------------------------------------------------------------------------
  // 7. Republish button → POST /api/expose/republish
  // -------------------------------------------------------------------------

  it('calls POST /api/expose/republish when republish button is clicked', async () => {
    mockGet.mockResolvedValue({ data: MOCK_CATALOG_ONE_DEVICE })
    mockPost.mockResolvedValue({ data: { ok: true, message: 'HA Discovery re-published.' } })

    renderExpose()

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

    fireEvent.click(screen.getByTestId('republish-button'))

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

    const postCall = mockPost.mock.calls[0]
    expect(postCall[0]).toBe('/api/expose/republish')
  })

  // -------------------------------------------------------------------------
  // 8. Republish ok=true → success alert; ok=false → warning
  // -------------------------------------------------------------------------

  it('shows success alert when republish returns ok=true', async () => {
    mockGet.mockResolvedValue({ data: MOCK_CATALOG_ONE_DEVICE })
    mockPost.mockResolvedValue({
      data: { ok: true, message: 'HA Discovery re-published successfully.' },
    })

    renderExpose()

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

    fireEvent.click(screen.getByTestId('republish-button'))

    await waitFor(() => {
      expect(screen.getByTestId('republish-success')).toBeInTheDocument()
    })
    expect(screen.queryByTestId('republish-warning')).not.toBeInTheDocument()
  })

  it('shows warning alert when republish returns ok=false', async () => {
    mockGet.mockResolvedValue({ data: MOCK_CATALOG_ONE_DEVICE })
    mockPost.mockResolvedValue({
      data: { ok: false, message: 'MQTT not connected.' },
    })

    renderExpose()

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

    fireEvent.click(screen.getByTestId('republish-button'))

    await waitFor(() => {
      expect(screen.getByTestId('republish-warning')).toBeInTheDocument()
    })
    expect(screen.queryByTestId('republish-success')).not.toBeInTheDocument()
  })

  // -------------------------------------------------------------------------
  // 9. No crash from non-serialisable value_getter (value_getter NOT rendered)
  // -------------------------------------------------------------------------

  it('renders without crashing even if value_getter were present (it is excluded by API)', async () => {
    // The API never sends value_getter; this tests resilience of the schema
    const catalogWithExtraField = {
      ...MOCK_CATALOG_ONE_DEVICE,
      catalog: MOCK_CATALOG_ONE_DEVICE.catalog.map((entry) => ({
        ...entry,
        entity: { ...entry.entity },
        // No value_getter — confirm the component handles normal data
      })),
    }
    mockGet.mockResolvedValue({ data: catalogWithExtraField })
    renderExpose()

    await waitFor(() => {
      expect(screen.getByTestId('expose-settings')).toBeInTheDocument()
    })
    // No errors thrown, entities visible
    expect(screen.getByText('Test Meter Voltage')).toBeInTheDocument()
  })
})
