/**
 * Tests for ConfigPage (M2-T08).
 *
 * 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).
 */

import { describe, it, expect, vi, beforeEach } 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 },
      ],
    },
  ],
}

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