import { expect, test, type Locator, type Page } from '@playwright/test'

async function dragBy(page: Page, locator: Locator, deltaX: number, deltaY: number) {
  const box = await locator.boundingBox()
  if (!box) throw new Error('Drag target is not visible')
  const start = { x: box.x + Math.min(30, box.width / 3), y: box.y + Math.min(14, box.height / 4) }
  await page.mouse.move(start.x, start.y)
  await page.mouse.down()
  await page.mouse.move(start.x + deltaX / 2, start.y + deltaY / 2, { steps: 3 })
  await page.mouse.move(start.x + deltaX, start.y + deltaY, { steps: 3 })
  await page.mouse.up()
}

async function boardPosition(locator: Locator) {
  return locator.evaluate(element => ({
    left: Number.parseFloat((element as HTMLElement).style.left),
    top: Number.parseFloat((element as HTMLElement).style.top),
  }))
}

async function waitForSave(page: Page, action: () => Promise<void>) {
  const response = page.waitForResponse(candidate => candidate.request().method() === 'PUT' && candidate.url().includes('/api/levels/') && candidate.ok())
  await action()
  await response
}

test('move, folder expansion, empty-board pan, desktop wheel zoom, mobile pinch, and reload persistence', async ({ page }) => {
  await page.goto('/?level=e2e-level&edit=1')
  await expect(page.getByRole('heading', { name: 'Browser Safety Test' })).toBeVisible()
  await expect(page.locator('.case-heading')).toHaveText('Browser Safety Test')
  await expect(page.getByRole('button', { name: 'ADMIN', exact: true })).toBeVisible()
  expect((await page.locator('.menubar nav > button, .menubar nav > .admin-menu > button').allTextContents()).map(label => label.replace(/\d+$/, ''))).toEqual(['EVIDENCE', 'BRIEF', 'HELP', 'ADMIN'])
  await expect(page.getByRole('button', { name: 'Timeline settings', exact: true })).toBeVisible()
  const temporalLayering = await page.evaluate(() => ({
    links: Number.parseInt(getComputedStyle(document.querySelector('.temporal-links')!).zIndex, 10),
    timeline: Number.parseInt(getComputedStyle(document.querySelector('.timeline')!).zIndex, 10),
  }))
  expect(temporalLayering.links).toBeGreaterThan(temporalLayering.timeline)
  expect(await page.locator('.timeline .year').first().evaluate(element => Number.parseFloat(getComputedStyle(element).fontSize))).toBeGreaterThanOrEqual(12)
  expect(await page.locator('.timeline-range').evaluate(element => Number.parseFloat(getComputedStyle(element).fontSize))).toBeGreaterThanOrEqual(11)
  await expect(page.locator('.documents-panel')).toHaveClass(/\bclosed\b/)
  await expect(page.locator('.brief-panel')).toBeVisible()
  await expect(page.getByRole('button', { name: 'Brief', exact: true }).locator('.brief-count')).toHaveCount(0)
  await page.getByRole('button', { name: 'Minimize brief', exact: true }).click()
  await expect(page.locator('.brief-panel')).toHaveClass(/\bminimized\b/)
  await expect(page.locator('.brief-panel > p')).toBeHidden()
  await expect(page.getByRole('button', { name: 'Close brief', exact: true })).toBeVisible()
  await page.getByRole('button', { name: 'Restore brief', exact: true }).click()
  await expect(page.locator('.brief-panel')).not.toHaveClass(/\bminimized\b/)
  await page.getByRole('button', { name: 'BEGIN INVESTIGATION', exact: true }).click()

  await page.getByRole('button', { name: 'EVIDENCE', exact: true }).click()
  const documentRow = page.locator('[data-document-row-id="22222222-2222-4222-8222-222222222222"]')
  await documentRow.locator('.doc-row-main').click()
  await expect(documentRow).toHaveClass(/\bselected\b/)
  await expect(page.locator('[data-temporal-id="widget:11111111-1111-4111-8111-111111111111"]')).toHaveClass(/\bdocument-located\b/)
  await expect(page.locator('.document-locator-beam .document-locator-ray')).toBeVisible()
  const transformBeforeLocate = await page.locator('.board').getAttribute('style')
  await waitForSave(page, () => page.getByRole('button', { name: 'Locate Dated source image on board', exact: true }).click())
  await expect.poll(() => page.locator('.board').getAttribute('style')).not.toEqual(transformBeforeLocate)
  await expect(documentRow).toHaveClass(/\bselected\b/)
  await documentRow.locator('.doc-row-main').dblclick()
  await expect(page.getByRole('button', { name: 'Close document', exact: true })).toBeVisible()
  await page.getByRole('button', { name: 'Close document', exact: true }).click()
  await page.getByRole('button', { name: 'Close documents', exact: true }).click()

  page.once('dialog', dialog => dialog.accept('Disposable working theory'))
  await waitForSave(page, () => page.getByRole('button', { name: 'NEW NOTE', exact: true }).click())
  const note = page.locator('.evidence-card.note').filter({ hasText: 'Disposable working theory' })
  const trash = page.locator('.board-trash')
  await expect(note).toBeVisible()
  const noteBox = await note.boundingBox(), trashBox = await trash.boundingBox()
  if (!noteBox || !trashBox) throw new Error('Note or exhibit trash is not visible')
  const noteDragStart = { x: noteBox.x + noteBox.width / 2, y: noteBox.y + noteBox.height / 2 }
  const discardSave = page.waitForResponse(candidate => candidate.request().method() === 'PUT' && candidate.url().includes('/api/levels/') && candidate.ok())
  await page.mouse.move(noteDragStart.x, noteDragStart.y)
  await page.mouse.down()
  await page.mouse.move(trashBox.x + trashBox.width / 2, trashBox.y + trashBox.height / 2, { steps: 8 })
  await expect(trash).toHaveClass(/\bactive\b/)
  await page.mouse.up()
  await discardSave
  await expect(note).toHaveCount(0)
  await page.reload()
  await expect(page.locator('.evidence-card.note')).toHaveCount(0)

  const folder = page.locator('[data-temporal-id="widget:11111111-1111-4111-8111-111111111111"]')
  const file = page.locator('[data-temporal-id="widget:22222222-2222-4222-8222-222222222222"]')
  const board = page.locator('.board')
  const boardViewport = page.locator('.board-viewport')
  const containmentBand = page.locator('.folder-bands line')
  await expect(folder).toBeVisible()
  await expect(containmentBand).toHaveClass(/\bclosed\b/)
  const folderBefore = await boardPosition(folder)

  await waitForSave(page, () => dragBy(page, folder, 70, 42))
  const movedFolder = await boardPosition(folder)
  expect(movedFolder.left).toBeGreaterThan(folderBefore.left)
  expect(movedFolder.top).toBeGreaterThan(folderBefore.top)

  await page.reload()
  await expect(folder).toBeVisible()
  expect(await boardPosition(folder)).toEqual(movedFolder)

  await waitForSave(page, () => folder.dblclick())
  await expect(file).toHaveClass(/\bopen\b/)
  await expect(containmentBand).toHaveClass(/\bopen\b/)
  const fileBefore = await boardPosition(file)
  await waitForSave(page, () => dragBy(page, file, 56, 35))
  const movedFile = await boardPosition(file)
  expect(movedFile.left).toBeGreaterThan(fileBefore.left)
  expect(movedFile.top).toBeGreaterThan(fileBefore.top)

  await page.reload()
  await expect(file).toHaveClass(/\bopen\b/)
  expect(await boardPosition(file)).toEqual(movedFile)

  const folderBeforePan = await boardPosition(folder)
  const transformBeforePan = await board.getAttribute('style')
  await expect(page.getByRole('button', { name: 'MOVE', exact: true })).toHaveClass(/\bactive\b/)
  const viewportBox = await boardViewport.boundingBox()
  if (!viewportBox) throw new Error('Board viewport is not visible')
  await waitForSave(page, async () => {
    await boardViewport.dispatchEvent('pointerdown', { pointerId: 31, pointerType: 'mouse', isPrimary: true, button: 0, clientX: viewportBox.x + 240, clientY: viewportBox.y + 230 })
    await boardViewport.dispatchEvent('pointermove', { pointerId: 31, pointerType: 'mouse', isPrimary: true, button: 0, clientX: viewportBox.x + 290, clientY: viewportBox.y + 195 })
    await boardViewport.dispatchEvent('pointerup', { pointerId: 31, pointerType: 'mouse', isPrimary: true, button: 0, clientX: viewportBox.x + 290, clientY: viewportBox.y + 195 })
  })
  expect(await boardPosition(folder)).toEqual(folderBeforePan)
  const transformAfterPan = await board.getAttribute('style')
  expect(transformAfterPan).not.toEqual(transformBeforePan)

  await page.reload()
  await expect(folder).toBeVisible()
  expect(await board.getAttribute('style')).toEqual(transformAfterPan)

  const transformBeforeWheel = await board.getAttribute('style')
  const browserMetricsBeforeWheel = await page.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight, devicePixelRatio: window.devicePixelRatio }))
  const folderAnchorBefore = await folder.boundingBox()
  if (!folderAnchorBefore) throw new Error('Folder is not visible for cursor-centered zoom')
  const wheelAnchor = { x: folderAnchorBefore.x + folderAnchorBefore.width / 2, y: folderAnchorBefore.y + folderAnchorBefore.height / 2 }
  await page.mouse.move(wheelAnchor.x, wheelAnchor.y)
  await page.mouse.wheel(0, -80)
  await expect.poll(() => board.getAttribute('style')).not.toEqual(transformBeforeWheel)
  const folderAnchorAfter = await folder.boundingBox()
  if (!folderAnchorAfter) throw new Error('Folder disappeared after zoom')
  expect(folderAnchorAfter.x + folderAnchorAfter.width / 2).toBeCloseTo(wheelAnchor.x, 0)
  expect(folderAnchorAfter.y + folderAnchorAfter.height / 2).toBeCloseTo(wheelAnchor.y, 0)
  expect(await page.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight, devicePixelRatio: window.devicePixelRatio }))).toEqual(browserMetricsBeforeWheel)

  const transformBeforeDesktopPinch = await board.getAttribute('style')
  await page.keyboard.down('Control')
  await page.mouse.wheel(0, -80)
  await page.keyboard.up('Control')
  await page.waitForTimeout(100)
  expect(await board.getAttribute('style')).toEqual(transformBeforeDesktopPinch)
  expect(await page.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight, devicePixelRatio: window.devicePixelRatio }))).toEqual(browserMetricsBeforeWheel)

  const transformBeforeTouchPinch = await board.getAttribute('style')
  const touchOrigin = { x: viewportBox.x + 350, y: viewportBox.y + 240 }
  await boardViewport.dispatchEvent('pointerdown', { pointerId: 41, pointerType: 'touch', isPrimary: true, button: 0, clientX: touchOrigin.x, clientY: touchOrigin.y })
  await boardViewport.dispatchEvent('pointerdown', { pointerId: 42, pointerType: 'touch', isPrimary: false, button: 0, clientX: touchOrigin.x + 100, clientY: touchOrigin.y })
  await boardViewport.dispatchEvent('pointermove', { pointerId: 42, pointerType: 'touch', isPrimary: false, button: 0, clientX: touchOrigin.x + 150, clientY: touchOrigin.y })
  await expect.poll(() => board.getAttribute('style')).not.toEqual(transformBeforeTouchPinch)
  await boardViewport.dispatchEvent('pointerup', { pointerId: 42, pointerType: 'touch', isPrimary: false, button: 0, clientX: touchOrigin.x + 150, clientY: touchOrigin.y })
  await boardViewport.dispatchEvent('pointerup', { pointerId: 41, pointerType: 'touch', isPrimary: true, button: 0, clientX: touchOrigin.x, clientY: touchOrigin.y })

  await page.getByRole('button', { name: 'Brief', exact: true }).click()
  await expect(page.locator('.brief-panel')).toContainText('Ada Lovelace')
  const personConcept = page.locator('.brief-concepts section').filter({ hasText: 'Ada Lovelace' })
  await waitForSave(page, () => personConcept.getByRole('button', { name: 'PERSON', exact: true }).click())
  await expect(page.locator('.brief-panel')).toBeVisible()
  await expect(page.locator('.evidence-card.party.arriving')).toContainText('Ada Lovelace')
  await personConcept.getByRole('button', { name: 'EDIT DOSSIER', exact: true }).click()
  await expect(page.getByText('Edit person dossier')).toBeVisible()
  await page.getByLabel('Party aliases').fill('A. A. L.')
  await page.locator('.party-editor .folder-members input[type="checkbox"]').first().check()
  await waitForSave(page, () => page.getByRole('button', { name: 'SAVE DOSSIER', exact: true }).click())
  await expect(page.locator('.evidence-card.party')).toContainText('Ada Lovelace')

  const organizationConcept = page.locator('.brief-concepts section').filter({ hasText: 'Difference Engine Bureau' })
  await waitForSave(page, () => organizationConcept.getByRole('button', { name: 'ORGANIZATION', exact: true }).click())
  await expect(page.locator('.brief-panel')).toBeVisible()
  await organizationConcept.getByRole('button', { name: 'EDIT DOSSIER', exact: true }).click()
  await page.getByLabel('Organization type').selectOption('public_body')
  await waitForSave(page, () => page.getByRole('button', { name: 'SAVE DOSSIER', exact: true }).click())
  await page.getByRole('button', { name: 'CREATE PARTY NOT LISTED ABOVE', exact: true }).click()
  await expect(page.getByText('Create party dossier', { exact: true })).toBeVisible()
  await page.getByLabel('Party type').selectOption('person')
  await page.getByLabel('Party name').fill('Mara Elise Voss')
  await page.getByLabel('Party summary').fill('Identified by the investigator outside the supplied concept list.')
  await page.locator('.party-editor .folder-members input[type="checkbox"]').first().check()
  await waitForSave(page, () => page.getByRole('button', { name: 'SAVE DOSSIER', exact: true }).click())
  await expect(page.locator('.brief-panel')).toBeVisible()
  await page.getByRole('button', { name: 'RETURN TO BOARD', exact: true }).click()
  await page.reload()
  await expect(page.locator('.brief-panel')).toBeHidden()
  await expect(page.locator('.evidence-card.party')).toHaveCount(3)
  await expect(page.locator('.evidence-card.party')).toContainText(['Ada Lovelace', 'Difference Engine Bureau', 'Mara Elise Voss'])

  await file.dblclick()
  await page.getByRole('button',{ name:'TYPE',exact:true }).click()
  await waitForSave(page,() => page.getByRole('menuitemradio',{ name:/Mugshot/ }).click())
  await expect(file.locator('.mugshot-caption')).toHaveText('')
  await page.getByRole('button',{ name:'Close document',exact:true }).click()
  const adaParty=page.locator('.evidence-card.party').filter({ hasText:'Ada Lovelace' })
  await adaParty.click()
  await page.getByRole('button',{ name:'Red thread' }).click()
  await file.click()
  await expect(page.getByLabel('Thread tag')).toHaveValue('Identified as…')
  await waitForSave(page,() => page.getByRole('button',{ name:'ADD TAG & TIGHTEN' }).click())
  await expect(file).toHaveAttribute('data-identified-party-id',/\S+/)
  await expect(file.locator('.mugshot-caption')).toContainText('Ada Lovelace')
  await page.reload()
  await expect(file.locator('.mugshot-caption')).toContainText('Ada Lovelace')
  await adaParty.getByRole('button',{ name:'EDIT DOSSIER',exact:true }).click()
  await page.getByLabel('Party name').fill('Ada Byron Lovelace')
  await waitForSave(page,() => page.getByRole('button',{ name:'SAVE DOSSIER',exact:true }).click())
  await expect(file.locator('.mugshot-caption')).toContainText('Ada Byron Lovelace')

  await page.getByRole('button', { name: 'NEW EVENT', exact: true }).click()
  await expect(page.getByText('Edit reconstructed event')).toBeVisible()
  await page.getByLabel('Event title').fill('The browser clue was connected')
  await page.getByLabel('Event narrative').fill('The investigator connected the folder to a dated source.')
  await page.locator('.event-support-list input[type="checkbox"]').first().check()
  await waitForSave(page, () => page.getByRole('button', { name: 'SAVE EVENT', exact: true }).click())
  await page.reload()
  await expect(page.locator('.evidence-card.event')).toContainText('The browser clue was connected')
  await expect(page.locator('.evidence-card.event')).toContainText('UNDATED')
  await expect(page.locator('.story-strip')).toContainText('The investigator connected the folder')
  await expect(page.locator('.story-strip')).toContainText('UNDATED')
  await expect(page.locator('.timeline .marker')).toHaveCount(1)
  await expect(page.locator('.event-support-lines line')).toHaveCount(1)

  page.once('dialog', dialog => dialog.accept('Browser Template'))
  const templateSaved = page.waitForResponse(candidate => candidate.request().method() === 'POST' && candidate.url().includes('/templates?edit=1') && candidate.ok())
  await page.getByRole('button', { name: 'ADMIN', exact: true }).click()
  await page.getByRole('menuitem', { name: 'SAVE AS TEMPLATE', exact: true }).click()
  await templateSaved
  await expect(page.locator('.terminal-status')).toContainText('VERSION 1')

  let promptIndex = 0
  const prompts = ['browser-template', 'Browser Template Clone']
  const promptHandler = (dialog: { accept(promptText?: string): Promise<void> }) => dialog.accept(prompts[promptIndex++])
  page.on('dialog', promptHandler)
  await page.getByRole('button', { name: 'ADMIN', exact: true }).click()
  await page.getByRole('menuitem', { name: 'NEW FROM TEMPLATE', exact: true }).click()
  await page.waitForURL(url => url.searchParams.get('level')?.startsWith('browser-template-') === true)
  page.off('dialog', promptHandler)
  await expect(page.getByRole('heading', { name: 'Browser Template Clone' })).toBeVisible()
  await expect(page.getByRole('button', { name: 'Reset' })).toBeEnabled()

  await page.setViewportSize({ width: 390, height: 844 })
  const portraitToolbar = await page.locator('.board-actions').boundingBox()
  if (!portraitToolbar) throw new Error('Portrait toolbar is not visible')
  expect(portraitToolbar.height).toBeGreaterThan(portraitToolbar.width * 2)
  expect(portraitToolbar.x).toBeGreaterThan(280)
  const portraitMove = await page.getByRole('button', { name: 'MOVE', exact: true }).boundingBox()
  const portraitParty = await page.getByRole('button', { name: 'NEW PARTY', exact: true }).boundingBox()
  expect(portraitMove).not.toBeNull()
  expect(portraitParty).not.toBeNull()
  expect(portraitParty!.y).toBeGreaterThan(portraitMove!.y)

  await page.setViewportSize({ width: 844, height: 390 })
  const landscapeToolbar = await page.locator('.board-actions').boundingBox()
  if (!landscapeToolbar) throw new Error('Landscape toolbar is not visible')
  expect(landscapeToolbar.width).toBeGreaterThan(landscapeToolbar.height * 2)
})

test('hides the admin menu without a verified admin JWT', async ({ browser }) => {
  const context = await browser.newContext({ baseURL: 'http://127.0.0.1:18788', viewport: { width: 1280, height: 720 }, extraHTTPHeaders: { Cookie: 'anonymous_session=1' } })
  const page = await context.newPage()
  await page.goto('/?level=e2e-level&edit=1')
  await expect(page.getByRole('heading', { name: 'Browser Safety Test' })).toBeVisible()
  await expect(page.getByRole('button', { name: 'ADMIN', exact: true })).toHaveCount(0)
  expect((await page.locator('.menubar nav > button').allTextContents()).map(label => label.replace(/\d+$/, ''))).toEqual(['EVIDENCE', 'BRIEF', 'HELP'])
  await context.close()
})
