import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { render, screen, cleanup, waitFor, fireEvent, act, within } from '@testing-library/react'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { useParams, useLocation } from 'react-router-dom'
import Game from '../pages/Game'
import { setPlayerDrawTower, setPlayer2DrawTower } from '../theme.ts'

const { mockEngine, mockTheme, mockNavigate } = vi.hoisted(() => ({
  mockNavigate: vi.fn(),
  mockTheme: {
    bgStr: '#0d1117',
    text: '#e6edf3',
    textMuted: '#8b949e',
    bgPanel: 'rgba(22,27,34,0.92)',
    border: '#21262d',
    player1Str: '#58a6ff',
    player2Str: '#f0883e',
    rematchBg: '#238636',
    rematchBgHover: '#2ea043',
    errorBg: 'rgba(35,10,10,0.92)',
    error: '#f85149',
    playing: '#58a6ff',
    waiting: '#8b949e',
    win: '#3fb950',
    draw: '#d29922',
    bg: '#0d1117',
  },
  mockEngine: {
    status: 'win',
    players: ['P1', 'P2'],
    currentPlayer: 0,
    playerIndex: 0,
    winnerPlayerId: '0',
    scores: { '0': { wins: 1 } },
    opponentOnline: true,
    isMuted: false,
    addCell: vi.fn(),
    applyMove: vi.fn(),
    rejectMove: vi.fn(),
    applyWin: vi.fn(),
    applyDraw: vi.fn(),
    applyRoomState: vi.fn(),
    playerLeft: vi.fn(),
    setOpponentOnline: vi.fn(),
    toggleMute: vi.fn(),
    placeLocalMove: vi.fn(),
    onStateChanged: vi.fn(),
    onError: vi.fn(),
  },
}))

vi.mock('react-router-dom', () => ({
  useParams: vi.fn(() => ({ roomId: 'local' })),
  useNavigate: vi.fn(() => mockNavigate),
  useLocation: vi.fn(() => ({ state: null })),
}))

vi.mock('../theme.ts', () => ({
  theme: mockTheme,
  THEMES: { midnight: { label: 'Midnight', emoji: '🌙' } },
  useThemeMode: vi.fn(() => ['midnight', vi.fn()]),
  getThemeMode: vi.fn(() => 'midnight'),
  getPlayerName: vi.fn(() => 'Caro 1'),
  setPlayerName: vi.fn(),
  getPlayer2Name: vi.fn(() => 'Caro 2'),
  setPlayer2Name: vi.fn(),
  getPlayerSymbol: vi.fn(() => 'X'),
  setPlayerSymbol: vi.fn(),
  getPlayer1Symbol: vi.fn(() => 'X'),
  setPlayer1Symbol: vi.fn(),
  getPlayer2Symbol: vi.fn(() => 'O'),
  setPlayer2Symbol: vi.fn(),
  getPlayerColor: vi.fn(() => null),
  setPlayerColor: vi.fn(),
  getPlayer2Color: vi.fn(() => null),
  setPlayer2Color: vi.fn(),
  getPlayerDrawTower: vi.fn(() => true),
  setPlayerDrawTower: vi.fn(),
  getPlayer2DrawTower: vi.fn(() => true),
  setPlayer2DrawTower: vi.fn(),
}))

vi.mock('../components/ThemeSelector.tsx', () => ({
  default: () => null,
}))

vi.mock('../components/SymbolPicker.tsx', () => ({
  default: () => null,
}))

vi.mock('../components/GameCanvas.tsx', () => ({
  default: ({ onEngineReady }: any) => {
    setTimeout(() => onEngineReady?.(mockEngine), 0)
    return <div data-testid="game-canvas" />
  },
}))

afterEach(() => {
  vi.useRealTimers()
  vi.unstubAllGlobals()
  cleanup()
})

beforeEach(() => {
  vi.clearAllMocks()
  vi.mocked(useParams).mockReturnValue({ roomId: 'test-room' })
  vi.mocked(useLocation).mockReturnValue({ state: null } as any)
  mockEngine.status = 'win'
  mockEngine.players = ['P1', 'P2']
  mockEngine.playerIndex = 0
  mockEngine.winnerPlayerId = '0'
  mockEngine.scores = { '0': { wins: 1 } }
  mockEngine.opponentOnline = true
  mockEngine.isMuted = false
  mockEngine.placeLocalMove.mockReset()
  mockNavigate.mockReset()
})

describe('Game page (with engine)', () => {
  it('renders HUD with flexWrap and maxWidth', async () => {
    render(<Game />)
    const hud = await waitFor(() => {
      const el = screen.getByText('test-room').closest('div')!
      expect(el).toHaveStyle('maxWidth: min(600px, 90vw)')
      return el
    })
    expect(hud).toHaveStyle('flexWrap: wrap')
  })

  it('renders edit name button with 36px touch target', async () => {
    render(<Game />)
    const editBtn = await screen.findByTitle('Change your name')
    expect(editBtn).toHaveStyle('minWidth: 36px')
    expect(editBtn).toHaveStyle('minHeight: 36px')
  })

  it('renders local player cards after engine initialization', async () => {
    vi.mocked(useParams).mockReturnValue({ roomId: 'local' })
    vi.mocked(useLocation).mockReturnValue({ state: { player1Name: 'P1', player2Name: 'P2' } } as any)
    ;(mockEngine as any).camera = { setInitialZoom: vi.fn() }
    ;(mockEngine as any).symbols = { customColors: [], drawTowers: [] }
    ;(mockEngine as any).updatePlayerConfig = vi.fn()
    ;(mockEngine as any).redraw = vi.fn()
    render(<Game />)
    expect(await screen.findByTitle('Edit Player 1')).toBeInTheDocument()
    expect(await screen.findByTitle('Edit Player 2')).toBeInTheDocument()
  })

  it('shows opponent disconnected banner with zIndex 6', async () => {
    render(<Game />)
    await screen.findByText('test-room')
    mockEngine.opponentOnline = false
    mockEngine.onStateChanged?.()
    await screen.findByText(/opponent disconnected/i)
    const banner = screen.getByText(/opponent disconnected/i).closest('div')!
    expect(banner).toHaveStyle('zIndex: 6')
  })

  it('shows local turn indicator with top z-index', async () => {
    vi.mocked(useParams).mockReturnValue({ roomId: 'local' })
    vi.mocked(useLocation).mockReturnValue({ state: { player1Name: 'P1', player2Name: 'P2' } } as any)
    ;(mockEngine as any).camera = { setInitialZoom: vi.fn() }
    ;(mockEngine as any).symbols = { customColors: [], drawTowers: [] }
    ;(mockEngine as any).updatePlayerConfig = vi.fn()
    ;(mockEngine as any).redraw = vi.fn()
    render(<Game />)
    const turnMsg = await screen.findByText(/Caro 1's turn/i)
    const turnDiv = turnMsg.closest('div')!
    expect(turnDiv).toHaveStyle('zIndex: 10')
  })

  it('opens name modal with Cancel/Save 44px buttons', async () => {
    render(<Game />)
    const editBtn = await screen.findByTitle('Change your name')
    editBtn.click()
    const cancelBtn = await screen.findByText('Cancel')
    expect(cancelBtn).toHaveStyle('minWidth: 44px')
    expect(cancelBtn).toHaveStyle('minHeight: 44px')
    const saveBtn = screen.getByText('Save')
    expect(saveBtn).toHaveStyle('minWidth: 44px')
    expect(saveBtn).toHaveStyle('minHeight: 44px')
  })

  it('renders name modal panel with min(320px, 85vw)', async () => {
    render(<Game />)
    const editBtn = await screen.findByTitle('Change your name')
    editBtn.click()
    const header = await screen.findByText('Change your name')
    const panel = header.parentElement!
    const src = readFileSync(resolve(__dirname, '../pages/Game.tsx'), 'utf-8')
    expect(src).toContain("width: 'min(320px, 85vw)'")
  })

  it('allows toggling the 3D tower effect in local mode player modal settings', async () => {
    vi.mocked(useParams).mockReturnValue({ roomId: 'local' })
    vi.mocked(useLocation).mockReturnValue({ state: { player1Name: 'Caro 1', player2Name: 'Caro 2' } } as any)

    // Setup mock properties for local mode
    ;(mockEngine as any).camera = { setInitialZoom: vi.fn() }
    ;(mockEngine as any).symbols = { customColors: [], drawTowers: [] }
    ;(mockEngine as any).updatePlayerConfig = vi.fn()
    ;(mockEngine as any).redraw = vi.fn()

    render(<Game />)

    // Wait for the game screen and engine initialization
    const editP1Btn = await screen.findByTitle('Edit Player 1')
    editP1Btn.click()

    // Verify modal header is rendered
    await screen.findByText('Edit Player 1 Settings')

    // Find and toggle the 3D Tower Effect checkbox
    const checkbox = screen.getByLabelText(/3D Tower Effect/i) as HTMLInputElement
    expect(checkbox.checked).toBe(true)

    // Click checkbox to uncheck
    checkbox.click()
    expect(checkbox.checked).toBe(false)

    // Save changes
    const saveBtn = screen.getByText('Save Changes')
    saveBtn.click()

    // Assert updatePlayerConfig has been called with drawTowers parameter [false, true]
    await waitFor(() => {
      expect(mockEngine.updatePlayerConfig).toHaveBeenLastCalledWith(
        ['Caro 1', 'Caro 2'],
        ['X', 'O'],
        [expect.any(Number), expect.any(Number)],
        [false, true]
      )
    })
  })

  it('renders built-in bot status in local mode', async () => {
    vi.mocked(useParams).mockReturnValue({ roomId: 'local' })
    vi.mocked(useLocation).mockReturnValue({
      state: {
        player1Name: 'Bot 1',
        player2Name: 'Caro 2',
        player1Type: 'bot',
        player2Type: 'human',
        rules: { swap2: false, noOverlines: false, boardSize: 15 },
      },
    } as any)

    ;(mockEngine as any).camera = { setInitialZoom: vi.fn() }
    ;(mockEngine as any).symbols = { customColors: [], drawTowers: [] }
    ;(mockEngine as any).updatePlayerConfig = vi.fn()
    ;(mockEngine as any).redraw = vi.fn()

    render(<Game />)

    await waitFor(() => expect((mockEngine as any).manager).toBeTruthy())
    await screen.findByText(/Caro 1's turn \(Bot\)/)
  })

  it('renders the Swap2 step guide and shrinks the board fit in local mode', async () => {
    vi.mocked(useParams).mockReturnValue({ roomId: 'local' })
    vi.mocked(useLocation).mockReturnValue({
      state: {
        player1Name: 'Caro 1',
        player2Name: 'Caro 2',
        player1Type: 'human',
        player2Type: 'human',
        rules: { swap2: true, noOverlines: true, boardSize: 15 },
      },
    } as any)

    ;(mockEngine as any).camera = { setInitialZoom: vi.fn() }
    ;(mockEngine as any).symbols = { customColors: [], drawTowers: [] }
    ;(mockEngine as any).updatePlayerConfig = vi.fn()
    ;(mockEngine as any).redraw = vi.fn()

    render(<Game />)

    const guide = await screen.findByLabelText('Swap2 steps')
    expect(guide).toBeInTheDocument()
    expect(screen.getByText('1. Place three stones')).toHaveStyle(`color: ${mockTheme.player1Str}`)
    expect(screen.getByText('2. Swap2 Choices')).toBeInTheDocument()
    expect(screen.getByText('3. Continue the game')).toBeInTheDocument()
    expect((mockEngine as any).camera.setInitialZoom).toHaveBeenCalledWith(15, 0.66)
  })

  it('hides the Swap2 guide after the first normal move following the choice', async () => {
    vi.mocked(useParams).mockReturnValue({ roomId: 'local' })
    vi.mocked(useLocation).mockReturnValue({
      state: {
        player1Name: 'Caro 1',
        player2Name: 'Caro 2',
        player1Type: 'human',
        player2Type: 'human',
        rules: { swap2: true, noOverlines: true, boardSize: 15 },
      },
    } as any)

    ;(mockEngine as any).camera = { setInitialZoom: vi.fn() }
    ;(mockEngine as any).symbols = { customColors: [], drawTowers: [] }
    ;(mockEngine as any).updatePlayerConfig = vi.fn()
    ;(mockEngine as any).redraw = vi.fn()

    render(<Game />)

    await screen.findByLabelText('Swap2 steps')
    act(() => {
      ;(mockEngine as any).manager.placeMove(7, 7)
      ;(mockEngine as any).manager.placeMove(8, 7)
      ;(mockEngine as any).manager.placeMove(9, 7)
      ;(mockEngine as any).manager.chooseSwap(false)
      ;(mockEngine as any).manager.placeMove(10, 7)
      ;(mockEngine as any).onStateChanged?.()
    })

    await waitFor(() => expect(screen.queryByLabelText('Swap2 steps')).not.toBeInTheDocument())
  })

  it('shows + New Bot while waiting and starts a local bot match', async () => {
    vi.mocked(useParams).mockReturnValue({ roomId: 'waiting-room' })
    vi.mocked(useLocation).mockReturnValue({ state: null } as any)
    mockEngine.status = 'waiting'
    mockEngine.players = ['Caro 1']

    const view = render(<Game />)

    const newBot = await view.findByText('+ New Bot')
    fireEvent.click(newBot)

    expect(mockNavigate).toHaveBeenCalledWith('/play/local', {
      state: {
        player1Name: 'Caro 1',
        player2Name: 'Bot 2',
        player1Symbol: 'X',
        player2Symbol: 'O',
        player1Type: 'human',
        player2Type: 'bot',
        botProfile: 'normal',
        rules: { swap2: true, noOverlines: true, boardSize: 15 },
      },
    })
  })

  it('calls the server bot endpoint before placing a local bot move', async () => {
    vi.mocked(useParams).mockReturnValue({ roomId: 'local' })
    vi.mocked(useLocation).mockReturnValue({
      state: {
        player1Name: 'Bot 1',
        player2Name: 'Caro 2',
        player1Type: 'bot',
        player2Type: 'human',
        botProfile: 'expert',
        rules: { swap2: false, noOverlines: false, boardSize: 15 },
      },
    } as any)

    const fetchMock = vi.fn(async () => ({
      ok: true,
      json: async () => ({ move: { x: 7, y: 7 }, source: 'alpha_beta' }),
    }))
    vi.stubGlobal('fetch', fetchMock)
    vi.useFakeTimers()

    ;(mockEngine as any).camera = { setInitialZoom: vi.fn() }
    ;(mockEngine as any).symbols = { customColors: [], drawTowers: [] }
    ;(mockEngine as any).updatePlayerConfig = vi.fn()
    ;(mockEngine as any).redraw = vi.fn()

    render(<Game />)
    await act(async () => {
      vi.advanceTimersByTime(0)
      await Promise.resolve()
    })
    expect((mockEngine as any).manager).toBeTruthy()
    fetchMock.mockClear()
    await act(async () => {
      ;(mockEngine as any).status = 'active'
      ;(mockEngine as any).currentPlayer = 0
      ;(mockEngine as any).onStateChanged?.()
      vi.advanceTimersByTime(221)
      await Promise.resolve()
      await Promise.resolve()
    })
    expect(fetchMock).toHaveBeenCalledWith('/api/bot/move', expect.objectContaining({
      method: 'POST',
      headers: { 'content-type': 'application/json' },
    }))

    const [, options] = fetchMock.mock.calls[0]
    const body = JSON.parse((options as RequestInit).body as string)
    expect(body.currentPlayer).toBe(0)
    expect(body.profile).toBe('expert')
    expect(body.rules).toEqual({ swap2: false, noOverlines: false, boardSize: 15 })
    expect(mockEngine.placeLocalMove).toHaveBeenCalledWith(7, 7)
  })

  it('swaps player symbols and colors next to the player cards when local swap is applied', async () => {
    vi.mocked(useParams).mockReturnValue({ roomId: 'local' })
    vi.mocked(useLocation).mockReturnValue({ state: { player1Name: 'Caro 1', player2Name: 'Caro 2' } } as any)

    // Setup mock properties for local mode
    ;(mockEngine as any).camera = { setInitialZoom: vi.fn() }
    ;(mockEngine as any).symbols = { customColors: [], drawTowers: [] }
    ;(mockEngine as any).updatePlayerConfig = vi.fn()
    ;(mockEngine as any).redraw = vi.fn()

    render(<Game />)

    // Wait for engine to initialize and edit buttons to render
    await screen.findByTitle('Edit Player 1')

    // Simulate engine swap values (names stay same, symbols swap)
    ;(mockEngine as any).players = ['Caro 1', 'Caro 2']
    ;(mockEngine as any).playerSymbols = ['O', 'X']

    // Simulate swap applied
    act(() => {
      ;(mockEngine as any).onSwapApplied?.()
    })

    // Now Player 1 Card should display "Caro 1" and "O"
    const card1 = await screen.findByTitle('Edit Player 1')
    expect(within(card1).getByText(/Caro 1/)).toBeInTheDocument()
    expect(within(card1).getByText('O')).toBeInTheDocument()

    // Now Player 2 Card should display "Caro 2" and "X"
    const card2 = await screen.findByTitle('Edit Player 2')
    expect(within(card2).getByText(/Caro 2/)).toBeInTheDocument()
    expect(within(card2).getByText('X')).toBeInTheDocument()
  })

  it('does not swap player colors or tower settings when local swap is applied with swapped=false', async () => {
    vi.mocked(useParams).mockReturnValue({ roomId: 'local' })
    vi.mocked(useLocation).mockReturnValue({ state: { player1Name: 'Caro 1', player2Name: 'Caro 2' } } as any)

    // Setup mock properties for local mode
    ;(mockEngine as any).camera = { setInitialZoom: vi.fn() }
    ;(mockEngine as any).symbols = { customColors: [], drawTowers: [] }
    ;(mockEngine as any).updatePlayerConfig = vi.fn()
    ;(mockEngine as any).redraw = vi.fn()

    render(<Game />)

    // Wait for engine to initialize and edit buttons to render
    await screen.findByTitle('Edit Player 1')

    // Reset calls on mock functions
    vi.mocked(setPlayerDrawTower).mockClear()
    vi.mocked(setPlayer2DrawTower).mockClear()

    // Simulate engine values (names and symbols stay same)
    ;(mockEngine as any).players = ['Caro 1', 'Caro 2']
    ;(mockEngine as any).playerSymbols = ['X', 'O']

    // Simulate swap applied with false (stay)
    act(() => {
      ;(mockEngine as any).onSwapApplied?.(false)
    })

    // Assert that tower settings were not updated (since no swap happened)
    expect(setPlayerDrawTower).not.toHaveBeenCalled()
    expect(setPlayer2DrawTower).not.toHaveBeenCalled()

    // Now Player 1 Card should still display "Caro 1" and "X"
    const card1 = await screen.findByTitle('Edit Player 1')
    expect(within(card1).getByText(/Caro 1/)).toBeInTheDocument()
    expect(within(card1).getByText('X')).toBeInTheDocument()

    // Now Player 2 Card should still display "Caro 2" and "O"
    const card2 = await screen.findByTitle('Edit Player 2')
    expect(within(card2).getByText(/Caro 2/)).toBeInTheDocument()
    expect(within(card2).getByText('O')).toBeInTheDocument()
  })
})
