/**
 * Tests for ConfigPage (M2-T08, M5-T01B).
 *
 * Strategy: vi.mock the apiClient module so we can control GET/PUT/POST responses
 * without a real server.
 *
 * Coverage:
 * 1. Renders config sections from a mocked GET /api/config response.
 * 2. Secret fields start as empty (never display masked value).
 * 3. Non-secret fields show their loaded values.
 * 4. Save: updates map includes all non-secret fields and excludes untouched secrets.
 * 5. Save: updates map includes a secret only when the user typed a new value.
 * 6. Save success → shows success notice.
 * 7. Save error → shows error notice.
 * 8. SMTP test button: success state (200 result=success).
 * 9. SMTP test button: config-error state (400/ApiError result=config-error).
 * 10. SMTP test button: failed state (502/ApiError result=failed).
 * 11. (M5-T01B) Accordion: sections rendered as accordion items.
 * 12. (M5-T01B) Accordion: second section can be expanded by clicking its control.
 */

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

// ---------------------------------------------------------------------------
// Fixture: config sections
// ---------------------------------------------------------------------------

const MOCK_CONFIG = {
  sections: [
    {
      name: 'General',
      fields: [
        { env_name: 'APP_NAME', label: 'App Name', value: 'My Home', secret: false, input_type: 'text', configured: true },
        { env_name: 'APP_PORT', label: 'Port', value: '8000', secret: false, input_type: 'number', configured: true },
      ],
    },
    {
      name: 'SMTP',
      fields: [
        { env_name: 'SMTP_HOST', label: 'SMTP Host', value: 'smtp.example.com', secret: false, input_type: 'text', configured: true },
        { env_name: 'SMTP_PASSWORD', label: 'SMTP Password', value: '', secret: true, input_type: 'password', configured: true },
      ],
    },
  ],
}

/** Extended fixture that includes a checkbox (bool) field. */
const MOCK_CONFIG_WITH_CHECKBOX = {
  sections: [
    {
      name: 'MQTT',
      fields: [
        { env_name: 'MQTT_ENABLED', label: 'MQTT Enabled', value: 'false', secret: false, input_type: 'checkbox', configured: true },
        { env_name: 'MQTT_BROKER_HOST', label: 'MQTT Broker Host', value: 'localhost', secret: false, input_type: 'text', configured: true },
      ],
    },
    {
      name: 'SMTP',
      fields: [
        { env_name: 'SMTP_HOST', label: 'SMTP Host', value: 'smtp.example.com', secret: false, input_type: 'text', configured: true },
        { env_name: 'SMTP_PASSWORD', label: 'SMTP Password', value: '', secret: true, input_type: 'password', configured: true },
      ],
    },
  ],
}

// ---------------------------------------------------------------------------
// 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(),
}))

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

function renderConfig() {
  return renderWithProviders(<ConfigPage />, { initialPath: '/config' })
}

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

describe('ConfigPage', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    // Default: GET /api/config returns the fixture
    mockGet.mockResolvedValue({ data: MOCK_CONFIG, response: { status: 200, ok: true } })
  })

  // -------------------------------------------------------------------------
  // 1. Renders sections
  // -------------------------------------------------------------------------

  it('renders section names and field labels', async () => {
    renderConfig()

    await waitFor(() => {
      expect(screen.getByText('General')).toBeInTheDocument()
    })

    expect(screen.getByText('SMTP')).toBeInTheDocument()
    expect(screen.getByText('App Name')).toBeInTheDocument()
    expect(screen.getByText('SMTP Host')).toBeInTheDocument()
    expect(screen.getByText('SMTP Password')).toBeInTheDocument()
  })

  // -------------------------------------------------------------------------
  // 2. Secret fields start empty
  // -------------------------------------------------------------------------

  it('renders secret fields with empty value (never displays masked value)', async () => {
    renderConfig()

    await waitFor(() => {
      expect(screen.getByText('SMTP Password')).toBeInTheDocument()
    })

    // Mantine puts data-testid on the <input> element itself
    const secretInput = screen.getByTestId('field-secret-SMTP_PASSWORD') as HTMLInputElement
    expect(secretInput.value).toBe('')
  })

  // -------------------------------------------------------------------------
  // 3. Non-secret fields show their loaded values
  // -------------------------------------------------------------------------

  it('renders non-secret fields with their loaded values', async () => {
    renderConfig()

    await waitFor(() => {
      expect(screen.getByTestId('field-APP_NAME')).toBeInTheDocument()
    })

    // Mantine puts data-testid on the <input> element itself for TextInput
    const appNameInput = screen.getByTestId('field-APP_NAME') as HTMLInputElement
    expect(appNameInput.value).toBe('My Home')

    const smtpHostInput = screen.getByTestId('field-SMTP_HOST') as HTMLInputElement
    expect(smtpHostInput.value).toBe('smtp.example.com')
  })

  // -------------------------------------------------------------------------
  // 4. Save: updates includes all non-secrets, excludes untouched secrets
  // -------------------------------------------------------------------------

  it('save sends all non-secret fields and excludes untouched (blank) secrets', async () => {
    mockPut.mockResolvedValueOnce({ data: {}, response: { status: 200, ok: true } })
    // After save, refetch
    mockGet.mockResolvedValue({ data: MOCK_CONFIG, response: { status: 200, ok: true } })

    renderConfig()

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

    // Submit without touching any field
    fireEvent.submit(screen.getByTestId('config-form'))

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

    const putCall = mockPut.mock.calls[0]
    const body = putCall[1].body as { updates: Record<string, string> }
    const updates = body.updates

    // Non-secret fields MUST be present
    expect(updates).toHaveProperty('APP_NAME', 'My Home')
    expect(updates).toHaveProperty('APP_PORT', '8000')
    expect(updates).toHaveProperty('SMTP_HOST', 'smtp.example.com')

    // Untouched secret field MUST NOT be present
    expect(updates).not.toHaveProperty('SMTP_PASSWORD')
  })

  // -------------------------------------------------------------------------
  // 5. Save: updates includes secret when user typed a new value
  // -------------------------------------------------------------------------

  it('save includes a secret field when the user typed a new value', async () => {
    mockPut.mockResolvedValueOnce({ data: {}, response: { status: 200, ok: true } })
    mockGet.mockResolvedValue({ data: MOCK_CONFIG, response: { status: 200, ok: true } })

    renderConfig()

    await waitFor(() => {
      expect(screen.getByTestId('field-secret-SMTP_PASSWORD')).toBeInTheDocument()
    })

    // Mantine puts data-testid on the <input> element itself
    const secretInput = screen.getByTestId('field-secret-SMTP_PASSWORD') as HTMLInputElement
    fireEvent.change(secretInput, { target: { value: 'new-secret-value' } })

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

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

    const putCall = mockPut.mock.calls[0]
    const body = putCall[1].body as { updates: Record<string, string> }
    const updates = body.updates

    // Secret MUST be included because the user typed a value
    expect(updates).toHaveProperty('SMTP_PASSWORD', 'new-secret-value')
    // Non-secrets still present
    expect(updates).toHaveProperty('APP_NAME', 'My Home')
  })

  // -------------------------------------------------------------------------
  // 6. Save success → shows success notice
  // -------------------------------------------------------------------------

  it('shows success alert after a successful save', async () => {
    mockPut.mockResolvedValueOnce({ data: {}, response: { status: 200, ok: true } })

    renderConfig()

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

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

    await waitFor(() => {
      expect(screen.getByTestId('save-success')).toBeInTheDocument()
    })

    expect(screen.queryByTestId('save-error')).not.toBeInTheDocument()
  })

  // -------------------------------------------------------------------------
  // 7. Save error → shows error notice
  // -------------------------------------------------------------------------

  it('shows error alert when save fails', async () => {
    const { ApiError } = await import('../api/client')
    mockPut.mockRejectedValueOnce(new ApiError(422, { detail: 'invalid value' }))

    renderConfig()

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

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

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

    expect(screen.queryByTestId('save-success')).not.toBeInTheDocument()
  })

  // -------------------------------------------------------------------------
  // 8. SMTP test button: success state
  // -------------------------------------------------------------------------

  it('shows success alert after SMTP test succeeds', async () => {
    mockPost.mockResolvedValueOnce({
      data: { result: 'success', message: 'Email delivered.' },
      response: { status: 200, ok: true },
    })

    renderConfig()

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

    fireEvent.click(screen.getByTestId('smtp-test-button'))

    await waitFor(() => {
      expect(screen.getByTestId('smtp-result-success')).toBeInTheDocument()
    })

    expect(screen.queryByTestId('smtp-result-config-error')).not.toBeInTheDocument()
    expect(screen.queryByTestId('smtp-result-failed')).not.toBeInTheDocument()
  })

  // -------------------------------------------------------------------------
  // 9. SMTP test button: config-error state (400)
  // -------------------------------------------------------------------------

  it('shows config-error alert when SMTP test returns config-error', async () => {
    const { ApiError } = await import('../api/client')
    mockPost.mockRejectedValueOnce(
      new ApiError(400, { result: 'config-error', message: 'SMTP host not configured.' }),
    )

    renderConfig()

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

    fireEvent.click(screen.getByTestId('smtp-test-button'))

    await waitFor(() => {
      expect(screen.getByTestId('smtp-result-config-error')).toBeInTheDocument()
    })

    expect(screen.queryByTestId('smtp-result-success')).not.toBeInTheDocument()
    expect(screen.queryByTestId('smtp-result-failed')).not.toBeInTheDocument()
  })

  // -------------------------------------------------------------------------
  // 10. SMTP test button: failed state (502)
  // -------------------------------------------------------------------------

  it('shows failed alert when SMTP test returns failed', async () => {
    const { ApiError } = await import('../api/client')
    mockPost.mockRejectedValueOnce(
      new ApiError(502, { result: 'failed', message: 'Connection refused.' }),
    )

    renderConfig()

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

    fireEvent.click(screen.getByTestId('smtp-test-button'))

    await waitFor(() => {
      expect(screen.getByTestId('smtp-result-failed')).toBeInTheDocument()
    })

    expect(screen.queryByTestId('smtp-result-success')).not.toBeInTheDocument()
    expect(screen.queryByTestId('smtp-result-config-error')).not.toBeInTheDocument()
  })

  // -------------------------------------------------------------------------
  // 11. (M5-T01B) Accordion: sections rendered as accordion items
  // -------------------------------------------------------------------------

  it('renders an accordion with one item per config section', async () => {
    renderConfig()

    await waitFor(() => {
      expect(screen.getByTestId('config-accordion')).toBeInTheDocument()
    })

    // Each section should have an accordion control
    expect(screen.getByTestId('accordion-control-General')).toBeInTheDocument()
    expect(screen.getByTestId('accordion-control-SMTP')).toBeInTheDocument()
  })

  // -------------------------------------------------------------------------
  // 12. (M5-T01B) Accordion: second section can be expanded by clicking its control
  // -------------------------------------------------------------------------

  it('expands a collapsed section when its accordion control is clicked', async () => {
    renderConfig()

    await waitFor(() => {
      expect(screen.getByTestId('accordion-control-SMTP')).toBeInTheDocument()
    })

    // The SMTP section control is clickable (the accordion item exists)
    const smtpControl = screen.getByTestId('accordion-control-SMTP')
    expect(smtpControl).toBeInTheDocument()

    // Click to expand the SMTP section
    fireEvent.click(smtpControl)

    // After clicking, the SMTP section fields should still be in the DOM
    // (Mantine accordion keeps content in DOM even when collapsed)
    await waitFor(() => {
      expect(screen.getByText('SMTP Password')).toBeInTheDocument()
    })
  })
})

// ---------------------------------------------------------------------------
// M5-fix2: MqttTestButton — Issue 1 (frontend)
// ---------------------------------------------------------------------------

describe('ConfigPage — MQTT test button', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    // Use the MOCK_CONFIG_WITH_CHECKBOX fixture which includes an MQTT section
    mockGet.mockResolvedValue({ data: MOCK_CONFIG_WITH_CHECKBOX, response: { status: 200, ok: true } })
  })

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

  it('renders MQTT test button when MQTT section is present', async () => {
    renderConfig()

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

  it('does not render MQTT test button when no MQTT section present', async () => {
    // Use a config fixture without MQTT section
    mockGet.mockResolvedValueOnce({ data: MOCK_CONFIG, response: { status: 200, ok: true } })
    renderConfig()

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

    // No MQTT section in MOCK_CONFIG → button absent
    // Wait a bit for any async renders
    await waitFor(() => {
      expect(screen.queryByTestId('mqtt-test-button')).not.toBeInTheDocument()
    })
  })

  it('shows success alert after MQTT test succeeds', async () => {
    mockPost.mockResolvedValueOnce({
      data: { result: 'success', message: 'Test message published.' },
      response: { status: 200, ok: true },
    })

    renderConfig()

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

    fireEvent.click(screen.getByTestId('mqtt-test-button'))

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

    expect(screen.queryByTestId('mqtt-result-config-error')).not.toBeInTheDocument()
    expect(screen.queryByTestId('mqtt-result-failed')).not.toBeInTheDocument()
  })

  it('shows config-error alert when MQTT test returns config-error', async () => {
    const { ApiError } = await import('../api/client')
    mockPost.mockRejectedValueOnce(
      new ApiError(400, { result: 'config-error', message: 'Broker host not configured.' }),
    )

    renderConfig()

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

    fireEvent.click(screen.getByTestId('mqtt-test-button'))

    await waitFor(() => {
      expect(screen.getByTestId('mqtt-result-config-error')).toBeInTheDocument()
    })

    expect(screen.queryByTestId('mqtt-result-success')).not.toBeInTheDocument()
    expect(screen.queryByTestId('mqtt-result-failed')).not.toBeInTheDocument()
  })

  it('shows failed alert when MQTT test returns failed (502)', async () => {
    const { ApiError } = await import('../api/client')
    mockPost.mockRejectedValueOnce(
      new ApiError(502, { result: 'failed', message: 'Connection refused.' }),
    )

    renderConfig()

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

    fireEvent.click(screen.getByTestId('mqtt-test-button'))

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

    expect(screen.queryByTestId('mqtt-result-success')).not.toBeInTheDocument()
    expect(screen.queryByTestId('mqtt-result-config-error')).not.toBeInTheDocument()
  })

  it('MQTT test button has type="button" and does not submit the config form', async () => {
    renderConfig()

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

    const mqttBtn = screen.getByTestId('mqtt-test-button')
    // The rendered button element should have type="button"
    expect(mqttBtn.getAttribute('type')).toBe('button')
  })
})

// ---------------------------------------------------------------------------
// M5-fix2: ExposeSettings in config accordion — Issue 3 (frontend)
// ---------------------------------------------------------------------------

describe('ConfigPage — ExposeSettings in main accordion', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockGet.mockResolvedValue({ data: MOCK_CONFIG, response: { status: 200, ok: true } })
  })

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

  it('renders Home Assistant Expose accordion item inside the config accordion', async () => {
    renderConfig()

    await waitFor(() => {
      expect(screen.getByTestId('config-accordion')).toBeInTheDocument()
    })

    // The Expose accordion item should be inside the main config-accordion
    const configAccordion = screen.getByTestId('config-accordion')
    const exposeControl = screen.getByTestId('accordion-control-expose')
    expect(configAccordion.contains(exposeControl)).toBe(true)
  })

  it('Expose accordion item appears before the Save button in DOM order', async () => {
    renderConfig()

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

    const exposeControl = screen.getByTestId('accordion-control-expose')
    const saveButton = screen.getByTestId('config-save-button')

    // compareDocumentPosition: if expose comes before save, position & Node.DOCUMENT_POSITION_FOLLOWING === true
    const position = exposeControl.compareDocumentPosition(saveButton)
    // DOCUMENT_POSITION_FOLLOWING = 4 means saveButton comes after exposeControl
    expect(position & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
  })
})

// ---------------------------------------------------------------------------
// Area B: checkbox (Switch) rendering and value round-trip
// ---------------------------------------------------------------------------

describe('ConfigPage — checkbox (bool) fields', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockGet.mockResolvedValue({ data: MOCK_CONFIG_WITH_CHECKBOX, response: { status: 200, ok: true } })
  })

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

  // -------------------------------------------------------------------------
  // 13. Checkbox field renders as a Switch (checked=false when value='false')
  // -------------------------------------------------------------------------

  it('renders a checkbox field as a Switch with checked=false when value is "false"', async () => {
    renderConfig()

    await waitFor(() => {
      expect(screen.getByTestId('field-MQTT_ENABLED')).toBeInTheDocument()
    })

    const switchEl = screen.getByTestId('field-MQTT_ENABLED') as HTMLInputElement
    // Mantine Switch renders as <input type="checkbox">
    expect(switchEl.type).toBe('checkbox')
    expect(switchEl.checked).toBe(false)
  })

  // -------------------------------------------------------------------------
  // 14. Toggling Switch to ON produces "true" in the save payload
  // -------------------------------------------------------------------------

  it('toggling Switch to checked sends "true" for that field in buildUpdates', async () => {
    mockPut.mockResolvedValueOnce({ data: {}, response: { status: 200, ok: true } })
    mockGet.mockResolvedValue({ data: MOCK_CONFIG_WITH_CHECKBOX, response: { status: 200, ok: true } })

    renderConfig()

    await waitFor(() => {
      expect(screen.getByTestId('field-MQTT_ENABLED')).toBeInTheDocument()
    })

    // Toggle the switch ON
    const switchEl = screen.getByTestId('field-MQTT_ENABLED') as HTMLInputElement
    fireEvent.click(switchEl)

    // Submit the form
    fireEvent.submit(screen.getByTestId('config-form'))

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

    const putCall = mockPut.mock.calls[0]
    const body = putCall[1].body as { updates: Record<string, string> }
    const updates = body.updates

    // The bool field must be sent as the string "true"
    expect(updates).toHaveProperty('MQTT_ENABLED', 'true')
    // Other non-secret fields still included
    expect(updates).toHaveProperty('MQTT_BROKER_HOST', 'localhost')
  })

  // -------------------------------------------------------------------------
  // 15. Switch starts checked=true when initial value is "true"
  // -------------------------------------------------------------------------

  it('renders Switch as checked when initial value is "true"', async () => {
    const configWithEnabled = {
      sections: [
        {
          name: 'MQTT',
          fields: [
            { env_name: 'MQTT_ENABLED', label: 'MQTT Enabled', value: 'true', secret: false, input_type: 'checkbox', configured: true },
          ],
        },
      ],
    }
    mockGet.mockResolvedValue({ data: configWithEnabled, response: { status: 200, ok: true } })

    renderConfig()

    await waitFor(() => {
      expect(screen.getByTestId('field-MQTT_ENABLED')).toBeInTheDocument()
    })

    const switchEl = screen.getByTestId('field-MQTT_ENABLED') as HTMLInputElement
    expect(switchEl.checked).toBe(true)
  })
})
