/**
 * Tests for energy/EnergyCharts.tsx
 *
 * Coverage:
 * 1. Renders chart title with device name.
 * 2. Shows loading state while readings/metrics are loading.
 * 3. Shows error state when either query fails.
 * 4. Shows empty state when no readings are in the time window.
 * 5. Renders chart container when data is available.
 * 6. Tolerates missing keys in payload (no crash when key absent).
 * 7. Time-range segmented control is rendered.
 *
 * NOTE: Recharts itself is NOT mocked — we let it render (jsdom-compatible
 * rendering), but we only assert on data-testid wrappers, not Recharts internals.
 * Recharts SVG rendering may produce warnings in jsdom; these are expected and
 * benign (jsdom lacks ResizeObserver / SVG layout).
 */

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

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

const UUID = 'device-uuid-test'
const DEVICE_NAME = 'SDM120 Main'

const METRICS_RESP = {
  profile: 'sdm120',
  metrics: [
    { key: 'voltage', label: 'Voltage', unit: 'V', device_class: 'voltage' },
    { key: 'current', label: 'Current', unit: 'A', device_class: 'current' },
  ],
}

const READING = {
  recorded_at: '2026-06-22T10:00:00Z',
  payload: { voltage: 230.2, current: 1.3 },
}

const READINGS_RESP = {
  items: [READING],
}

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

const mockGet = vi.fn()

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

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

function setupDefaultMocks() {
  mockGet.mockImplementation((path: string) => {
    if (path === '/api/modbus/devices/{uuid}/metrics') {
      return Promise.resolve({ data: METRICS_RESP })
    }
    if (path === '/api/modbus/devices/{uuid}/readings') {
      return Promise.resolve({ data: READINGS_RESP })
    }
    return Promise.resolve({ data: null })
  })
}

function renderChart() {
  return renderWithProviders(
    <EnergyCharts uuid={UUID} deviceName={DEVICE_NAME} />,
    { initialPath: '/energy' },
  )
}

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

describe('EnergyCharts — rendering', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    setupDefaultMocks()
  })

  it('renders chart title with device name', async () => {
    renderChart()

    await waitFor(() => {
      expect(screen.getByTestId(`energy-charts-title-${UUID}`)).toBeInTheDocument()
    })

    expect(screen.getByTestId(`energy-charts-title-${UUID}`).textContent).toBe(DEVICE_NAME)
  })

  it('renders time-range segmented control', async () => {
    renderChart()

    await waitFor(() => {
      expect(screen.getByTestId(`energy-charts-preset-${UUID}`)).toBeInTheDocument()
    })
  })

  it('shows loading state while data is being fetched', () => {
    // Never resolve
    mockGet.mockReturnValue(new Promise(() => {}))
    renderChart()

    expect(screen.getByTestId(`energy-charts-loading-${UUID}`)).toBeInTheDocument()
  })

  it('shows error state when readings query fails', async () => {
    mockGet.mockRejectedValue(new Error('Network error'))
    renderChart()

    await waitFor(() => {
      expect(screen.getByTestId(`energy-charts-error-${UUID}`)).toBeInTheDocument()
    })
  })

  it('shows empty state when no readings in time window', async () => {
    mockGet.mockImplementation((path: string) => {
      if (path === '/api/modbus/devices/{uuid}/metrics') {
        return Promise.resolve({ data: METRICS_RESP })
      }
      if (path === '/api/modbus/devices/{uuid}/readings') {
        return Promise.resolve({ data: { items: [] } })
      }
      return Promise.resolve({ data: null })
    })

    renderChart()

    await waitFor(() => {
      expect(screen.getByTestId(`energy-charts-empty-${UUID}`)).toBeInTheDocument()
    })
  })

  it('renders chart container when data is available', async () => {
    renderChart()

    await waitFor(() => {
      expect(screen.getByTestId(`energy-charts-chart-${UUID}`)).toBeInTheDocument()
    })
  })
})

describe('EnergyCharts — truncation notice', () => {
  beforeEach(() => vi.clearAllMocks())

  it('shows truncation notice when readings count equals the limit cap (1000)', async () => {
    // Build exactly 1000 synthetic readings to trigger the truncation hint.
    const truncatedItems = Array.from({ length: 1000 }, (_, i) => ({
      recorded_at: new Date(Date.now() - (999 - i) * 5000).toISOString(),
      payload: { voltage: 230 + i * 0.01 },
    }))

    mockGet.mockImplementation((path: string) => {
      if (path === '/api/modbus/devices/{uuid}/metrics') {
        return Promise.resolve({ data: METRICS_RESP })
      }
      if (path === '/api/modbus/devices/{uuid}/readings') {
        return Promise.resolve({ data: { items: truncatedItems } })
      }
      return Promise.resolve({ data: null })
    })

    renderChart()

    await waitFor(() => {
      expect(
        screen.getByTestId(`energy-charts-truncated-${UUID}`),
      ).toBeInTheDocument()
    })
  })

  it('does NOT show truncation notice when readings count is below limit', async () => {
    // Default mock has exactly 1 item — well below 1000.
    setupDefaultMocks()
    renderChart()

    await waitFor(() => {
      expect(screen.getByTestId(`energy-charts-chart-${UUID}`)).toBeInTheDocument()
    })

    expect(screen.queryByTestId(`energy-charts-truncated-${UUID}`)).not.toBeInTheDocument()
  })
})

describe('EnergyCharts — payload key tolerance', () => {
  beforeEach(() => vi.clearAllMocks())

  it('does not crash when payload is missing a metric key', async () => {
    mockGet.mockImplementation((path: string) => {
      if (path === '/api/modbus/devices/{uuid}/metrics') {
        return Promise.resolve({ data: METRICS_RESP })
      }
      if (path === '/api/modbus/devices/{uuid}/readings') {
        // payload is missing 'current' key
        return Promise.resolve({
          data: { items: [{ recorded_at: '2026-06-22T10:00:00Z', payload: { voltage: 230.2 } }] },
        })
      }
      return Promise.resolve({ data: null })
    })

    // Should render without throwing
    expect(() => renderChart()).not.toThrow()

    await waitFor(() => {
      expect(screen.getByTestId(`energy-charts-${UUID}`)).toBeInTheDocument()
    })
  })

  it('does not crash when payload is entirely empty', async () => {
    mockGet.mockImplementation((path: string) => {
      if (path === '/api/modbus/devices/{uuid}/metrics') {
        return Promise.resolve({ data: METRICS_RESP })
      }
      if (path === '/api/modbus/devices/{uuid}/readings') {
        return Promise.resolve({
          data: { items: [{ recorded_at: '2026-06-22T10:00:00Z', payload: {} }] },
        })
      }
      return Promise.resolve({ data: null })
    })

    expect(() => renderChart()).not.toThrow()

    // All values null → empty state (no chart) or chart with no data points shown.
    await waitFor(() => {
      expect(screen.getByTestId(`energy-charts-${UUID}`)).toBeInTheDocument()
    })
  })
})

// ---------------------------------------------------------------------------
// Auto-refresh: rolling window (A2 — the core bug fix)
// ---------------------------------------------------------------------------

describe('EnergyCharts — auto-refresh rolling window', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  afterEach(() => {
    vi.clearAllMocks()
  })

  /**
   * EnergyCharts now passes spanMs (not fixed start/end) to useReadings.
   * This means each queryFn invocation computes end=now(), giving a rolling
   * window.  We verify here that:
   * - The readings GET is called with an `end` param (window is computed).
   * - Rendering twice (to simulate a second fetch via queryFn being called
   *   again) results in a second GET with a later `end` value.
   *
   * We test this by manually invoking the apiClient.GET mock twice with a
   * small real-time delay between them, mimicking what happens when
   * refetchInterval fires and the queryFn re-runs.
   */
  it('uses spanMs-based rolling window so each queryFn call gets a fresh end=now()', async () => {
    const endTimestamps: string[] = []

    mockGet.mockImplementation((path: string, opts: { params?: { query?: { end?: string } } } = {}) => {
      if (path === '/api/modbus/devices/{uuid}/metrics') {
        return Promise.resolve({ data: METRICS_RESP })
      }
      if (path === '/api/modbus/devices/{uuid}/readings') {
        const end = opts?.params?.query?.end
        if (end) endTimestamps.push(end)
        return Promise.resolve({ data: READINGS_RESP })
      }
      return Promise.resolve({ data: null })
    })

    // First render: triggers initial fetch (end=now at T0).
    const t0 = Date.now()
    renderWithProviders(
      <EnergyCharts uuid={UUID} deviceName={DEVICE_NAME} />,
      { initialPath: '/energy' },
    )

    // Wait for the initial readings GET to fire.
    await waitFor(() => expect(endTimestamps.length).toBeGreaterThanOrEqual(1))

    // The recorded `end` must not be before the test started.
    const firstEndMs = new Date(endTimestamps[0]).getTime()
    expect(firstEndMs).toBeGreaterThanOrEqual(t0)

    // The readings request must include an `end` query param (window is computed, not omitted).
    expect(endTimestamps[0]).toBeTruthy()
  })

  it('does not freeze the window on mount — window end advances across separate renders', async () => {
    // We simulate two independent hook invocations (as if refetch fired).
    // Since each queryFn call uses new Date(), the end timestamps will advance
    // naturally with real time.  We just need to verify the concept holds.
    const endTimestamps: string[] = []

    mockGet.mockImplementation((path: string, opts: { params?: { query?: { end?: string } } } = {}) => {
      if (path === '/api/modbus/devices/{uuid}/metrics') {
        return Promise.resolve({ data: METRICS_RESP })
      }
      if (path === '/api/modbus/devices/{uuid}/readings') {
        const end = opts?.params?.query?.end
        if (end) endTimestamps.push(end)
        return Promise.resolve({ data: READINGS_RESP })
      }
      return Promise.resolve({ data: null })
    })

    const { unmount } = renderWithProviders(
      <EnergyCharts uuid={UUID} deviceName={DEVICE_NAME} />,
      { initialPath: '/energy' },
    )

    await waitFor(() => expect(endTimestamps.length).toBeGreaterThanOrEqual(1))

    const firstEnd = new Date(endTimestamps[0]).getTime()

    // Unmount and re-mount to simulate a new queryFn invocation after a tick.
    unmount()
    vi.clearAllMocks()
    endTimestamps.length = 0

    // Small real-time delay to ensure new Date() will be >= first.
    await new Promise((resolve) => setTimeout(resolve, 5))

    mockGet.mockImplementation((path: string, opts: { params?: { query?: { end?: string } } } = {}) => {
      if (path === '/api/modbus/devices/{uuid}/metrics') {
        return Promise.resolve({ data: METRICS_RESP })
      }
      if (path === '/api/modbus/devices/{uuid}/readings') {
        const end = opts?.params?.query?.end
        if (end) endTimestamps.push(end)
        return Promise.resolve({ data: READINGS_RESP })
      }
      return Promise.resolve({ data: null })
    })

    renderWithProviders(
      <EnergyCharts uuid={UUID} deviceName={DEVICE_NAME} />,
      { initialPath: '/energy' },
    )

    await waitFor(() => expect(endTimestamps.length).toBeGreaterThanOrEqual(1))

    const secondEnd = new Date(endTimestamps[0]).getTime()

    // The second invocation's end must be >= the first — window is not frozen.
    expect(secondEnd).toBeGreaterThanOrEqual(firstEnd)
  })
})
