import { expect, request, test, type Browser, type BrowserContext, type Page } from 'playwright/test'

const liveEnabled = process.env.PLAYWRIGHT_LIVE === '1'
const apiBaseURL = process.env.E2E_LIVE_API_BASE_URL ?? 'http://127.0.0.1:19000'
const password = process.env.E2E_LIVE_PASSWORD ?? 'Passw0rd!'
const configuredUsers = (process.env.E2E_LIVE_USERS ?? '')
  .split(',')
  .map((item) => item.trim())
  .filter(Boolean)

type PlayerPage = {
  context: BrowserContext
  page: Page
  username: string
  password: string
}

test.skip(!liveEnabled, 'set PLAYWRIGHT_LIVE=1 to run live integration flow')

test('live room flow: create, join, ready, start, ding que, multi-turn discard', async ({ browser }) => {
  test.setTimeout(180_000)

  const api = await request.newContext({
    baseURL: apiBaseURL,
    extraHTTPHeaders: {
      'Content-Type': 'application/json',
    },
  })

  const players: PlayerPage[] = []

  try {
    await expectLiveStackReady(api)

    if (configuredUsers.length > 0) {
      for (const username of configuredUsers) {
        players.push(
          await openPlayerPage(browser, {
            username,
            password,
          }),
        )
      }
    } else {
      const sessions = await Promise.all(
        Array.from({ length: 4 }, (_, index) => createLiveUserSession(api, index)),
      )
      for (const session of sessions) {
        players.push(await openPlayerPage(browser, session))
      }
    }

    const [owner, guest2, guest3, guest4] = players

    await owner.page.goto('/hall')
    await expect(owner.page.getByTestId('open-create-room')).toBeVisible()
    await owner.page.getByTestId('open-create-room').click()
    await owner.page.getByTestId('create-room-name').fill(`live-room-${Date.now()}`)
    const createRoomResponsePromise = owner.page.waitForResponse((response) => {
      return response.url().includes('/api/v1/game/mahjong/room/create') && response.request().method() === 'POST'
    })
    await owner.page.getByTestId('submit-create-room').click()

    const createRoomResponse = await createRoomResponsePromise
    if (!createRoomResponse.ok()) {
      throw new Error(
        `create room failed: status=${createRoomResponse.status()} body=${await createRoomResponse.text()} request=${createRoomResponse.request().postData() ?? ''}`,
      )
    }
    const createRoomPayload = (await createRoomResponse.json()) as { data?: { room_id?: string; name?: string } }
    const roomID = createRoomPayload.data?.room_id
    if (!roomID) {
      throw new Error('live room id not found after creating room')
    }
    const roomName = createRoomPayload.data?.name ?? ''

    const enterCreatedRoomButton = owner.page.getByTestId('enter-created-room')
    if (await enterCreatedRoomButton.isVisible().catch(() => false)) {
      await enterCreatedRoomButton.click()
    } else {
      await owner.page.goto(`/game/chengdu/${roomID}${roomName ? `?roomName=${encodeURIComponent(roomName)}` : ''}`)
    }

    await expect(owner.page).toHaveURL(new RegExp(`/game/chengdu/${roomID}`))

    for (const guest of [guest2, guest3, guest4]) {
      await guest.page.goto('/hall')
      await expect(guest.page.getByTestId('quick-join-room-id')).toBeVisible()
      await guest.page.getByTestId('quick-join-room-id').fill(roomID)
      await guest.page.getByTestId('quick-join-submit').click()
      await expect(guest.page).toHaveURL(new RegExp(`/game/chengdu/${roomID}(\\?.*)?$`))
    }

    for (const player of players) {
      await expect(player.page.getByTestId('ready-toggle')).toBeVisible()
      await player.page.getByTestId('ready-toggle').click()
    }

    await expect(owner.page.getByTestId('start-game')).toBeEnabled({ timeout: 20_000 })
    await owner.page.getByTestId('start-game').click()

    const dingQueChoices: Array<'w' | 't' | 'b'> = ['w', 't', 'b', 'w']
    for (const [index, player] of players.entries()) {
      const suit = dingQueChoices[index]
      const dingQueButton = player.page.getByTestId(`ding-que-${suit}`)
      await expect(dingQueButton).toBeVisible({ timeout: 20_000 })
      await expect(dingQueButton).toBeEnabled()
      await dingQueButton.click()
    }

    const discardActors: string[] = []
    let previousActor: PlayerPage | null = null

    for (let turn = 0; turn < 4; turn += 1) {
      const actor = await findDiscardActor(players, 30_000, previousActor)
      const actorTiles = actor.page.locator('[data-testid^="hand-tile-"]')
      expect(await actorTiles.count()).toBeGreaterThan(0)
      expect(await countEnabledTiles(actor.page)).toBeGreaterThan(0)

      await actorTiles.first().click()
      await expect
        .poll(() => countEnabledTiles(actor.page), { timeout: 20_000 })
        .toBe(0)

      await resolvePendingClaims(players, 10_000)
      await drawIfNeeded(players, 10_000)

      discardActors.push(actor.username)
      previousActor = actor
    }

    expect(new Set(discardActors).size).toBeGreaterThan(1)
  } finally {
    await api.dispose()
    await Promise.all(players.map(async ({ context }) => context.close()))
  }
})

async function expectLiveStackReady(api: Awaited<ReturnType<typeof request.newContext>>): Promise<void> {
  const response = await api.get('/healthz')
  expect(response.ok()).toBeTruthy()
}

async function createLiveUserSession(
  api: Awaited<ReturnType<typeof request.newContext>>,
  index: number,
): Promise<{ username: string; password: string }> {
  const seed = `${Date.now()}-${index}-${Math.random().toString(36).slice(2, 8)}`
  const username = `pwlive_${seed}`
  const timestampDigits = String(Date.now()).slice(-8)
  const randomDigit = Math.floor(Math.random() * 10)
  const phone = `13${timestampDigits}${index}${randomDigit}`
  const email = `${username}@example.com`

  const registerResponse = await api.post('/api/v1/auth/register', {
    data: {
      username,
      phone,
      email,
      password,
    },
  })
  expect(registerResponse.ok(), await registerResponse.text()).toBeTruthy()

  const loginResponse = await api.post('/api/v1/auth/login', {
    data: {
      login_id: username,
      password,
    },
  })
  expect(loginResponse.ok(), await loginResponse.text()).toBeTruthy()

  return {
    username,
    password,
  }
}

async function openPlayerPage(
  browser: Browser,
  session: { username: string; password: string },
): Promise<PlayerPage> {
  const context = await browser.newContext()
  const page = await context.newPage()
  await page.goto('/login')
  await page.getByTestId('login-id').fill(session.username)
  await page.getByTestId('login-password').fill(session.password)
  const loginResponsePromise = page.waitForResponse((response) => {
    return response.url().includes('/api/v1/auth/login') && response.request().method() === 'POST'
  })
  await page.getByTestId('login-submit').click()
  const loginResponse = await loginResponsePromise
  if (!loginResponse.ok()) {
    throw new Error(
      `browser login failed: status=${loginResponse.status()} body=${await loginResponse.text()} request=${loginResponse.request().postData() ?? ''}`,
    )
  }
  await expect(page).toHaveURL(/\/hall$/)
  return {
    context,
    page,
    username: session.username,
    password: session.password,
  }
}

async function findDiscardActor(
  players: PlayerPage[],
  timeoutMs: number,
  previousActor?: PlayerPage | null,
): Promise<PlayerPage> {
  const deadline = Date.now() + timeoutMs

  while (Date.now() < deadline) {
    for (const player of players) {
      if (previousActor && player.username === previousActor.username) {
        continue
      }
      const firstTile = player.page.locator('[data-testid^="hand-tile-"]').first()
      const tileCount = await player.page.locator('[data-testid^="hand-tile-"]').count()
      if (tileCount === 0) {
        continue
      }
      if (await firstTile.isEnabled().catch(() => false)) {
        return player
      }
    }
    await players[0]?.page.waitForTimeout(250)
  }

  const diagnostics = await Promise.all(
    players.map(async (player) => {
      const logs = await player.page.locator('.sidebar-line').allTextContents().catch(() => [])
      const claimBarVisible = await player.page.getByTestId('claim-action-bar').isVisible().catch(() => false)
      const passVisible = await player.page.getByTestId('claim-pass').isVisible().catch(() => false)
      const enabledTiles = await countEnabledTiles(player.page)
      return [
        `player=${player.username}`,
        `enabledTiles=${enabledTiles}`,
        `claimBar=${claimBarVisible}`,
        `claimPass=${passVisible}`,
        `logs=${logs.slice(0, 4).join(' || ')}`,
      ].join(' ')
    }),
  )

  throw new Error(`no player reached enabled discard state within timeout\n${diagnostics.join('\n')}`)
}

async function countEnabledTiles(page: Page): Promise<number> {
  return page.locator('[data-testid^="hand-tile-"]:enabled').count()
}

async function resolvePendingClaims(players: PlayerPage[], timeoutMs: number): Promise<void> {
  const deadline = Date.now() + timeoutMs

  while (Date.now() < deadline) {
    let handled = false

    for (const player of players) {
      const passButton = player.page.getByTestId('claim-pass')
      if (await passButton.isVisible().catch(() => false)) {
        await expect(passButton).toBeEnabled({ timeout: 5_000 })
        await passButton.click()
        handled = true
      }
    }

    if (!handled) {
      return
    }

    await players[0]?.page.waitForTimeout(300)
  }
}

async function drawIfNeeded(players: PlayerPage[], timeoutMs: number): Promise<void> {
  const deadline = Date.now() + timeoutMs

  while (Date.now() < deadline) {
    for (const player of players) {
      const drawButton = player.page.getByTestId('draw-tile')
      if (await drawButton.isVisible().catch(() => false)) {
        await expect(drawButton).toBeEnabled({ timeout: 5_000 })
        await drawButton.click()
        return
      }
    }

    await players[0]?.page.waitForTimeout(250)
  }
}
