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

const BACKEND = 'http://localhost:5000'

async function setTutorialEnabled(request: APIRequestContext, enabled: boolean): Promise<void> {
  const res = await request.patch(`${BACKEND}/user/tutorial-progress`, {
    data: { enabled },
  })
  if (!res.ok()) {
    throw new Error(
      `Failed to set tutorial enabled=${enabled}: ${res.status()} ${await res.text()}`,
    )
  }
}

async function resetTutorialProgress(request: APIRequestContext): Promise<void> {
  const res = await request.patch(`${BACKEND}/user/tutorial-progress`, {
    data: { reset: true },
  })
  if (!res.ok()) {
    throw new Error(`Failed to reset tutorial progress: ${res.status()} ${await res.text()}`)
  }
}

async function deleteAllChildren(request: APIRequestContext): Promise<void> {
  const listRes = await request.get(`${BACKEND}/child/list`)
  const data = await listRes.json()
  for (const child of data.children ?? []) {
    await request.delete(`${BACKEND}/child/${child.id}`)
  }
}

async function createChild(request: APIRequestContext, name: string, age: number): Promise<string> {
  const res = await request.put(`${BACKEND}/child/add`, {
    data: { name, age, image_id: 'boy01' },
  })
  if (!res.ok()) {
    throw new Error(`Failed to create child ${name}: ${res.status()} ${await res.text()}`)
  }
  // /child/add returns a message but not the created child, so read the list.
  const id = await getFirstChildId(request)
  if (!id) {
    throw new Error(`Child ${name} was not found after creation`)
  }
  return id
}

async function getFirstChildId(request: APIRequestContext): Promise<string | null> {
  const res = await request.get(`${BACKEND}/child/list`)
  const data = await res.json()
  return data.children?.[0]?.id ?? null
}

async function ensureChild(request: APIRequestContext, name: string, age: number): Promise<string> {
  const existingId = await getFirstChildId(request)
  if (existingId) return existingId
  return createChild(request, name, age)
}

async function getFirstChoreId(request: APIRequestContext): Promise<string | null> {
  const res = await request.get(`${BACKEND}/chore/list`)
  const data = await res.json()
  return data.tasks?.[0]?.id ?? null
}

async function createChore(
  request: APIRequestContext,
  name: string,
  points: number,
): Promise<string> {
  const res = await request.put(`${BACKEND}/chore/add`, {
    data: { name, points, image_id: 'boy01' },
  })
  if (!res.ok()) {
    throw new Error(`Failed to create chore ${name}: ${res.status()} ${await res.text()}`)
  }
  const id = await getFirstChoreId(request)
  if (!id) {
    throw new Error(`Chore ${name} was not found after creation`)
  }
  return id
}

async function assignChoreToChild(
  request: APIRequestContext,
  childId: string,
  choreId: string,
): Promise<void> {
  const res = await request.post(`${BACKEND}/child/${childId}/assign-task`, {
    data: { task_id: choreId },
  })
  if (!res.ok()) {
    throw new Error(
      `Failed to assign chore ${choreId} to child ${childId}: ${res.status()} ${await res.text()}`,
    )
  }
}

async function createRoutine(
  request: APIRequestContext,
  name: string,
  points: number,
): Promise<string> {
  const res = await request.put(`${BACKEND}/routine/add`, {
    data: { name, points, image_id: 'boy01' },
  })
  if (!res.ok()) {
    throw new Error(`Failed to create routine ${name}: ${res.status()} ${await res.text()}`)
  }
  const data = await res.json()
  const id = data.routine?.id
  if (!id) {
    throw new Error(`Routine ${name} was not found after creation`)
  }
  return id
}

async function assignRoutineToChild(
  request: APIRequestContext,
  childId: string,
  routineId: string,
): Promise<void> {
  const res = await request.post(`${BACKEND}/child/${childId}/assign-routine`, {
    data: { routine_id: routineId },
  })
  if (!res.ok()) {
    throw new Error(
      `Failed to assign routine ${routineId} to child ${childId}: ${res.status()} ${await res.text()}`,
    )
  }
}

function getTutorialCard(page: Page) {
  return page.locator('.tutorial-root .card')
}

function getTutorialTitle(page: Page) {
  return page.locator('.tutorial-root .card .title')
}

async function expectTutorialCard(page: Page, title: string): Promise<void> {
  await expect(getTutorialCard(page)).toBeVisible({ timeout: 10000 })
  await expect(getTutorialTitle(page)).toHaveText(title)
}

async function dismissTutorial(page: Page): Promise<void> {
  const card = getTutorialCard(page)
  if (await card.isVisible({ timeout: 1000 }).catch(() => false)) {
    // Use "Cancel" to clear the active step and drain the queue in one
    // action, avoiding chained steps that would keep the card visible.
    await page.locator('.tutorial-root .btn-skip').click()
    await expect(card).not.toBeVisible({ timeout: 5000 })
  }
}

async function clickTutorialNext(page: Page): Promise<void> {
  await page.locator('.tutorial-root .btn-primary').click()
}

/**
 * Dismiss a tutorial by clicking the primary button repeatedly until the card
 * disappears. This walks through any chained steps without setting sessionSkipped.
 */
async function dismissTutorialChain(page: Page, maxClicks = 10): Promise<void> {
  const card = getTutorialCard(page)
  for (let i = 0; i < maxClicks; i++) {
    if (!(await card.isVisible().catch(() => false))) return
    await page.locator('.tutorial-root .btn-primary').click()
    await page.waitForTimeout(200)
  }
  await expect(card).not.toBeVisible({ timeout: 3000 })
}

test.describe('Tutorial system', () => {
  // Tutorial state is global to the signed-in user; run these tests sequentially
  // so parallel resets do not interfere with each other.
  test.describe.configure({ mode: 'serial' })

  test.beforeEach(async ({ request }) => {
    await deleteAllChildren(request)
    await setTutorialEnabled(request, false)
    await resetTutorialProgress(request)
    await setTutorialEnabled(request, true)
    await resetTutorialProgress(request)
  })

  test.afterEach(async ({ page, request }) => {
    await dismissTutorial(page)
    await setTutorialEnabled(request, false)
    await resetTutorialProgress(request)
  })

  test('children list shows create-child tutorial when no children exist', async ({
    page,
    request,
  }) => {
    // Retry deletion + navigation so a concurrent test that creates a child
    // does not leave us on the child-points tutorial instead of create-child.
    await expect(async () => {
      await deleteAllChildren(request)
      await page.goto('/parent')
      await expect(page).toHaveURL('/parent')
      await expectTutorialCard(page, 'Add your child')
    }).toPass({ timeout: 20000 })
  })

  test('help button shows parent-children-list chain on the children list', async ({
    page,
    request,
  }) => {
    const childName = 'TutorialKid'
    await ensureChild(request, childName, 7)
    await page.goto('/parent')
    await expect(page).toHaveURL('/parent')

    // Wait for the child card to render ( tolerate duplicate names from repeat runs ).
    await expect(page.getByText(childName, { exact: true }).first()).toBeVisible({ timeout: 10000 })

    // The children list may auto-show a brief loading-state hint. Clear any
    // active chain so the help button is reachable.
    await dismissTutorialChain(page)

    await page.getByRole('button', { name: 'Show help for this screen' }).click()

    await expectTutorialCard(page, 'Your children')
    await clickTutorialNext(page)

    await expectTutorialCard(page, 'Points')
    await clickTutorialNext(page)

    await expectTutorialCard(page, 'Tap a child')
  })

  test('create chore form shows edit-chore-name tutorial', async ({ page }) => {
    await page.goto('/parent/tasks/chores/create')
    await expect(page).toHaveURL('/parent/tasks/chores/create')
    await expectTutorialCard(page, 'Chore Name')
  })

  test('help button shows list-chore-help on the chore list', async ({ page }) => {
    await page.goto('/parent/tasks/chores')
    await expect(page).toHaveURL('/parent/tasks/chores')

    // No auto-shown tutorial should appear on this page.
    await expect(getTutorialCard(page)).not.toBeVisible({ timeout: 3000 })

    const helpButton = page.getByRole('button', { name: 'Show help for this screen' })
    await expect(helpButton).toBeVisible()
    await helpButton.click()

    await expectTutorialCard(page, 'Create your chore')
  })

  test('help button chains through list-edit-hint on the chore list', async ({ page }) => {
    await page.goto('/parent/tasks/chores')
    await expect(page).toHaveURL('/parent/tasks/chores')

    await page.getByRole('button', { name: 'Show help for this screen' }).click()
    await expectTutorialCard(page, 'Create your chore')

    await clickTutorialNext(page)
    await expectTutorialCard(page, 'Edit items')
  })

  test('help button shows select-child on the child detail page', async ({ page, request }) => {
    const childName = 'TutorialKid'
    const childId = await ensureChild(request, childName, 7)
    await page.goto(`/parent/${childId}`)
    await expect(page).toHaveURL(`/parent/${childId}`)

    // Wait for the child data to load and the assign buttons to render.
    await expect(page.getByText(childName, { exact: true }).first()).toBeVisible({ timeout: 10000 })
    await expect(page.getByRole('button', { name: 'Assign Chores' }).first()).toBeVisible({
      timeout: 10000,
    })

    // The page auto-shows select-child on first visit. Walk through the chain
    // so the help button is reachable and sessionSkipped stays false.
    await dismissTutorialChain(page)

    await page.getByRole('button', { name: 'Show help for this screen' }).click()
    await expectTutorialCard(page, "This is your child's page")

    await clickTutorialNext(page)
    await expectTutorialCard(page, 'Assign chores')
  })

  test('kebab menu on assigned chore shows chore-kebab-menu tutorial', async ({
    page,
    request,
  }) => {
    const childName = 'TutorialKid'
    const childId = await ensureChild(request, childName, 7)
    const choreId = await createChore(request, 'TutorialChore', 10)
    await assignChoreToChild(request, childId, choreId)

    await page.goto(`/parent/${childId}`)
    await expect(page).toHaveURL(`/parent/${childId}`)

    // Wait for the child data and the assigned chore to render.
    await expect(page.getByText(childName, { exact: true }).first()).toBeVisible({ timeout: 10000 })
    await expect(page.getByText('TutorialChore').first()).toBeVisible({ timeout: 10000 })

    // Clear the auto-shown select-child chain.
    await dismissTutorialChain(page)

    // Click the chore card to make it ready (reveals the kebab button).
    await page.locator('.item-card').filter({ hasText: 'TutorialChore' }).first().click()

    // Click the kebab button and verify the chore kebab tutorial fires.
    const kebabButton = page
      .locator('.kebab-btn')
      .filter({ has: page.locator('text=⋮') })
      .first()
    await expect(kebabButton).toBeVisible({ timeout: 5000 })
    await kebabButton.click()

    await expectTutorialCard(page, 'Chore actions')

    await clickTutorialNext(page)
    await expectTutorialCard(page, 'Edit points')

    await clickTutorialNext(page)
    await expectTutorialCard(page, 'Change schedule')
  })

  test('kebab menu on assigned routine shows routine-kebab-menu tutorial', async ({
    page,
    request,
  }) => {
    const childName = 'TutorialKid'
    const childId = await ensureChild(request, childName, 7)
    const routineId = await createRoutine(request, 'TutorialRoutine', 15)
    await assignRoutineToChild(request, childId, routineId)

    await page.goto(`/parent/${childId}`)
    await expect(page).toHaveURL(`/parent/${childId}`)

    // Wait for the child data and the assigned routine to render.
    await expect(page.getByText(childName, { exact: true }).first()).toBeVisible({ timeout: 10000 })
    await expect(page.getByText('TutorialRoutine').first()).toBeVisible({ timeout: 10000 })

    // Clear the auto-shown select-child chain.
    await dismissTutorialChain(page)

    // Click the routine card to make it ready (reveals the kebab button).
    await page.locator('.item-card').filter({ hasText: 'TutorialRoutine' }).first().click()

    // Click the kebab button and verify the routine kebab tutorial fires.
    const kebabButton = page
      .locator('.kebab-btn')
      .filter({ has: page.locator('text=⋮') })
      .first()
    await expect(kebabButton).toBeVisible({ timeout: 5000 })
    await kebabButton.click()

    await expectTutorialCard(page, 'Routine actions')

    await clickTutorialNext(page)
    await expectTutorialCard(page, 'Edit routine')

    await clickTutorialNext(page)
    await expectTutorialCard(page, 'Edit points')

    await clickTutorialNext(page)
    await expectTutorialCard(page, 'Change schedule')
  })

  test('dismissing an auto-shown tutorial persists across reloads', async ({ page, request }) => {
    await deleteAllChildren(request)
    await page.goto('/parent')
    await expectTutorialCard(page, 'Add your child')

    await page.locator('.tutorial-root .btn-primary').click()
    await expect(getTutorialCard(page)).not.toBeVisible({ timeout: 5000 })

    await page.reload()
    await expect(getTutorialCard(page)).not.toBeVisible({ timeout: 3000 })
  })

  test('add-child FAB is disabled while the create-child tutorial is showing', async ({
    page,
    request,
  }) => {
    await deleteAllChildren(request)
    await page.goto('/parent')
    await expectTutorialCard(page, 'Add your child')

    const fab = page.locator('.fab')
    await expect(fab).toBeVisible()
    await expect(fab).toBeDisabled()

    // The page should still be on the children list and the tutorial visible.
    await expect(page).toHaveURL('/parent')
    await expect(getTutorialCard(page)).toBeVisible()
  })

  test('child form inputs are disabled while the edit-child-name tutorial is showing', async ({
    page,
  }) => {
    await page.goto('/parent/children/create')
    await expect(page).toHaveURL('/parent/children/create')
    await expectTutorialCard(page, "Child's Name")

    await expect(page.locator('input#name')).toBeDisabled()
    await expect(page.locator('input#age')).toBeDisabled()

    // The tutorial card should remain visible after checking the inputs.
    await expect(getTutorialCard(page)).toBeVisible()
  })

  test('clicking the highlighted Points area does not navigate while child-points tutorial is showing', async ({
    page,
    request,
  }) => {
    const childName = 'TutorialPointsKid'
    await ensureChild(request, childName, 7)
    await page.goto('/parent')
    await expect(page).toHaveURL('/parent')

    // Wait for the child card to render so the children list has finished
    // loading before we assert on the Points tutorial.
    await expect(page.getByText(childName, { exact: true }).first()).toBeVisible({ timeout: 10000 })

    await expectTutorialCard(page, 'Points')

    // Small resilience delay so the spotlight blocker is positioned before the
    // synthetic click reaches it.
    await page.waitForTimeout(50)

    const points = page.locator('.card .points').first()
    await expect(points).toBeVisible()
    const box = await points.boundingBox()
    if (!box) throw new Error('Could not resolve points element bounding box')

    // Click the center of the highlighted points area. The tutorial spotlight
    // blocker should intercept the click, preventing the card click handler
    // from navigating to the child detail page.
    await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2)

    await expect(page).toHaveURL('/parent')
    await expect(getTutorialCard(page)).toBeVisible()
  })
})
