/**
 * Tests for LoginPage (M2-T07, extended M4-T08).
 *
 * Strategy: vi.mock the apiClient module so we can control POST /api/auth/login
 * responses without a real server.  We also mock useSession so tests can control
 * the authentication state.
 *
 * Coverage:
 * 1. Renders the login form (step 1).
 * 2. Successful login (step 1) → invalidates session query + navigates.
 * 3. 401 bad credentials → shows inline error, does not navigate.
 * 4. Already-authenticated users visiting /login → redirected to '/'.
 * 5. Unexpected error → shows generic error message.
 * 6. (M4-T08) 401 with totp_required → switches to step 2 TOTP form.
 * 7. (M4-T08) Step 2 submit with correct code → success + navigate.
 * 8. (M4-T08) Step 2 submit with wrong code → shows error, stays on step 2.
 * 9. (M4-T08) Step 2 "Back to login" button → returns to step 1.
 * 10. (M4-T08) 429 response → shows "too many attempts" message.
 */

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

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

// We mock the entire api/client module. Each test can override POST as needed.
const mockPost = vi.fn()

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

// ---------------------------------------------------------------------------
// Mock useSession — default: unauthenticated
// ---------------------------------------------------------------------------

// Typed as returning the wider union so mockReturnValue accepts all status variants.
const mockUseSession = vi.fn(() => ({
  status: 'unauthenticated' as 'loading' | 'authenticated' | 'unauthenticated',
  user: null as null | { username: string; force_password_change: boolean },
}))

vi.mock('../auth/SessionProvider', () => ({
  useSession: () => mockUseSession(),
  SessionProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}))

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

function renderLogin(initialPath = '/login') {
  return renderWithProviders(<LoginPage />, {
    initialPath,
    routes: [{ path: '/', element: <div data-testid="home-page">Home</div> }],
  })
}

function fillAndSubmit(username: string, password: string) {
  fireEvent.change(screen.getByTestId('username-input'), { target: { value: username } })
  fireEvent.change(screen.getByTestId('password-input'), { target: { value: password } })
  fireEvent.submit(screen.getByTestId('login-form'))
}

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

describe('LoginPage', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    // Reset to unauthenticated by default
    mockUseSession.mockReturnValue({ status: 'unauthenticated', user: null })
  })

  // -------------------------------------------------------------------------
  // 1. Renders the form
  // -------------------------------------------------------------------------

  it('renders the login form with username and password fields', () => {
    renderLogin()
    expect(screen.getByTestId('login-form')).toBeInTheDocument()
    expect(screen.getByTestId('username-input')).toBeInTheDocument()
    expect(screen.getByTestId('password-input')).toBeInTheDocument()
    expect(screen.getByTestId('login-submit')).toBeInTheDocument()
  })

  it('shows Sign In heading', () => {
    renderLogin()
    expect(screen.getByRole('heading', { name: /sign in/i })).toBeInTheDocument()
  })

  // -------------------------------------------------------------------------
  // 2. Successful login
  // -------------------------------------------------------------------------

  it('navigates to "/" on successful login', async () => {
    mockPost.mockResolvedValueOnce({
      data: { user: { username: 'admin', force_password_change: false }, csrf_token: 'tok123' },
      response: { status: 200, ok: true },
    })

    renderLogin()
    fillAndSubmit('admin', 'correct-password')

    await waitFor(() => {
      expect(screen.getByTestId('home-page')).toBeInTheDocument()
    })
  })

  it('calls POST /api/auth/login with the correct body', async () => {
    mockPost.mockResolvedValueOnce({
      data: { user: { username: 'admin', force_password_change: false }, csrf_token: 'tok123' },
      response: { status: 200, ok: true },
    })

    renderLogin()
    fillAndSubmit('myuser', 'mypassword')

    await waitFor(() => {
      expect(mockPost).toHaveBeenCalledWith('/api/auth/login', {
        body: { username: 'myuser', password: 'mypassword' },
      })
    })
  })

  // -------------------------------------------------------------------------
  // 3. 401 bad credentials
  // -------------------------------------------------------------------------

  it('shows inline error on 401 and does NOT navigate', async () => {
    mockPost.mockResolvedValueOnce({
      data: undefined,
      error: { detail: 'invalid username or password' },
      response: { status: 401, ok: false },
    })

    renderLogin()
    fillAndSubmit('admin', 'wrong-password')

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

    expect(screen.getByTestId('login-error')).toHaveTextContent(
      /incorrect username or password/i,
    )
    // Should still be on the login form, not navigated away
    expect(screen.getByTestId('login-form')).toBeInTheDocument()
  })

  it('does not include the password in the error message', async () => {
    mockPost.mockResolvedValueOnce({
      data: undefined,
      error: { detail: 'invalid username or password' },
      response: { status: 401, ok: false },
    })

    renderLogin()
    fillAndSubmit('admin', 'super-secret-password')

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

    expect(screen.getByTestId('login-error')).not.toHaveTextContent('super-secret-password')
  })

  // -------------------------------------------------------------------------
  // 5. Unexpected error
  // -------------------------------------------------------------------------

  it('shows generic error on unexpected network failure', async () => {
    mockPost.mockRejectedValueOnce(new Error('Network error'))

    renderLogin()
    fillAndSubmit('admin', 'password')

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

    expect(screen.getByTestId('login-error')).toHaveTextContent(/login failed/i)
  })

  // -------------------------------------------------------------------------
  // 4. Already authenticated
  // -------------------------------------------------------------------------

  it('redirects already-authenticated users to "/"', async () => {
    mockUseSession.mockReturnValue({
      status: 'authenticated',
      user: { username: 'admin', force_password_change: false },
    })

    renderLogin()

    await waitFor(() => {
      expect(screen.getByTestId('home-page')).toBeInTheDocument()
    })
  })

  // -------------------------------------------------------------------------
  // 6. (M4-T08) 401 with totp_required → step 2
  // -------------------------------------------------------------------------

  it('switches to TOTP step on 401 with totp_required=true', async () => {
    mockPost.mockResolvedValueOnce({
      data: undefined,
      error: { detail: { totp_required: true } },
      response: { status: 401, ok: false },
    })

    renderLogin()
    fillAndSubmit('admin', 'correct-password')

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

    // Step 1 form should be gone
    expect(screen.queryByTestId('login-form')).not.toBeInTheDocument()
    // TOTP code input should be visible
    expect(screen.getByTestId('totp-code-input')).toBeInTheDocument()
  })

  // -------------------------------------------------------------------------
  // 7. (M4-T08) Step 2 correct code → success
  // -------------------------------------------------------------------------

  it('completes login on correct TOTP code (step 2)', async () => {
    // Step 1: trigger totp_required
    mockPost.mockResolvedValueOnce({
      data: undefined,
      error: { detail: { totp_required: true } },
      response: { status: 401, ok: false },
    })
    // Step 2: success
    mockPost.mockResolvedValueOnce({
      data: { user: { username: 'admin', force_password_change: false }, csrf_token: 'tok456' },
      response: { status: 200, ok: true },
    })

    renderLogin()
    fillAndSubmit('admin', 'correct-password')

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

    // Enter the TOTP code and submit step 2
    fireEvent.change(screen.getByTestId('totp-code-input'), { target: { value: '123456' } })
    fireEvent.submit(screen.getByTestId('login-totp-form'))

    await waitFor(() => {
      expect(screen.getByTestId('home-page')).toBeInTheDocument()
    })

    // Check step 2 request included totp_code
    expect(mockPost).toHaveBeenCalledWith('/api/auth/login', {
      body: { username: 'admin', password: 'correct-password', totp_code: '123456' },
    })
  })

  // -------------------------------------------------------------------------
  // 8. (M4-T08) Step 2 wrong code → error, stays on step 2
  // -------------------------------------------------------------------------

  it('shows error on wrong TOTP code and stays on step 2', async () => {
    // Step 1: totp_required
    mockPost.mockResolvedValueOnce({
      data: undefined,
      error: { detail: { totp_required: true } },
      response: { status: 401, ok: false },
    })
    // Step 2: wrong code → 401
    mockPost.mockResolvedValueOnce({
      data: undefined,
      error: { detail: 'invalid totp code' },
      response: { status: 401, ok: false },
    })

    renderLogin()
    fillAndSubmit('admin', 'correct-password')

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

    fireEvent.change(screen.getByTestId('totp-code-input'), { target: { value: '000000' } })
    fireEvent.submit(screen.getByTestId('login-totp-form'))

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

    // Should still be on TOTP form
    expect(screen.getByTestId('login-totp-form')).toBeInTheDocument()
    expect(screen.getByTestId('login-error')).toHaveTextContent(/incorrect verification code/i)
  })

  // -------------------------------------------------------------------------
  // 9. (M4-T08) Step 2 "Back to login" → returns to step 1
  // -------------------------------------------------------------------------

  it('returns to step 1 when "Back to login" is clicked', async () => {
    mockPost.mockResolvedValueOnce({
      data: undefined,
      error: { detail: { totp_required: true } },
      response: { status: 401, ok: false },
    })

    renderLogin()
    fillAndSubmit('admin', 'correct-password')

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

    fireEvent.click(screen.getByTestId('totp-back'))

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

    expect(screen.queryByTestId('login-totp-form')).not.toBeInTheDocument()
  })

  // -------------------------------------------------------------------------
  // 10. (M4-T08) 429 → "too many attempts" message
  // -------------------------------------------------------------------------

  it('shows "too many attempts" message on 429', async () => {
    const { ApiError } = await import('../api/client')
    mockPost.mockRejectedValueOnce(new ApiError(429, { detail: 'Too Many Requests' }))

    renderLogin()
    fillAndSubmit('admin', 'password')

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

    expect(screen.getByTestId('login-error')).toHaveTextContent(/too many login attempts/i)
  })
})
