/**
 * Tests for ChangePasswordPage (M2-T07 rework-1).
 *
 * Strategy: vi.mock the apiClient and useSession modules so we can control
 * POST /api/auth/password responses and session state without a real server.
 *
 * Coverage:
 * 1. Renders the change-password form when user has force_password_change=true.
 * 2. Successful password change → navigates to '/' (proceeds into the app).
 * 3. Client-side mismatch → shows error, does NOT call the API.
 * 4. API 400 error → shows generic error, stays on form.
 * 5. Guard: non-forced user visiting /change-password → redirected to '/'.
 */

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

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

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: forced-change user
// ---------------------------------------------------------------------------

const mockUseSession = vi.fn(() => ({
  status: 'authenticated' as 'loading' | 'authenticated' | 'unauthenticated',
  user: { username: 'admin', force_password_change: true } as
    | null
    | { username: string; force_password_change: boolean },
}))

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

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

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

function fillAndSubmit(currentPw: string, newPw: string, confirmPw: string) {
  fireEvent.change(screen.getByTestId('current-password-input'), {
    target: { value: currentPw },
  })
  fireEvent.change(screen.getByTestId('new-password-input'), {
    target: { value: newPw },
  })
  fireEvent.change(screen.getByTestId('confirm-password-input'), {
    target: { value: confirmPw },
  })
  fireEvent.submit(screen.getByTestId('change-password-form'))
}

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

describe('ChangePasswordPage', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    // Default: authenticated user with force_password_change=true
    mockUseSession.mockReturnValue({
      status: 'authenticated',
      user: { username: 'admin', force_password_change: true },
    })
  })

  it('renders the change-password form for a forced-change user', () => {
    renderChangePw()
    expect(screen.getByTestId('change-password-form')).toBeInTheDocument()
    expect(screen.getByTestId('current-password-input')).toBeInTheDocument()
    expect(screen.getByTestId('new-password-input')).toBeInTheDocument()
    expect(screen.getByTestId('confirm-password-input')).toBeInTheDocument()
    expect(screen.getByTestId('change-password-submit')).toBeInTheDocument()
  })

  it('navigates to "/" after a successful password change', async () => {
    // Simulate successful POST /api/auth/password
    mockPost.mockResolvedValueOnce({
      data: {},
      response: { status: 200, ok: true },
    })

    renderChangePw()
    fillAndSubmit('old-password', 'new-password', 'new-password')

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

  it('calls POST /api/auth/password with the correct body', async () => {
    mockPost.mockResolvedValueOnce({
      data: {},
      response: { status: 200, ok: true },
    })

    renderChangePw()
    fillAndSubmit('current123', 'newpass456', 'newpass456')

    await waitFor(() => {
      expect(mockPost).toHaveBeenCalledWith('/api/auth/password', {
        body: {
          current_password: 'current123',
          new_password: 'newpass456',
          confirm_password: 'newpass456',
        },
      })
    })
  })

  it('shows error and does NOT call the API when new passwords do not match', async () => {
    renderChangePw()
    fillAndSubmit('current-pw', 'new-pw-1', 'new-pw-2')

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

    expect(screen.getByTestId('change-password-error')).toHaveTextContent(
      /do not match/i,
    )
    expect(mockPost).not.toHaveBeenCalled()
    // Should remain on the form
    expect(screen.getByTestId('change-password-form')).toBeInTheDocument()
  })

  it('shows generic error on API 400 and stays on form', async () => {
    // Simulate 400 via ApiError throw (as the client middleware does)
    const { ApiError } = await import('../api/client')
    mockPost.mockRejectedValueOnce(new ApiError(400, { detail: 'wrong password' }))

    renderChangePw()
    fillAndSubmit('wrong-current', 'newpass', 'newpass')

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

    expect(screen.getByTestId('change-password-error')).toHaveTextContent(
      /password change failed/i,
    )
    // Should NOT have navigated away
    expect(screen.getByTestId('change-password-form')).toBeInTheDocument()
  })

  it('redirects a non-forced user away from /change-password to "/"', async () => {
    // A user who has already changed their password
    mockUseSession.mockReturnValue({
      status: 'authenticated',
      user: { username: 'admin', force_password_change: false },
    })

    renderChangePw()

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

    // The change-password form must NOT be shown
    expect(screen.queryByTestId('change-password-form')).not.toBeInTheDocument()
  })
})
