import { World, setWorldConstructor } from '@cucumber/cucumber'
import {
  chromium,
  type Browser,
  type BrowserContext,
  type Page,
} from '@playwright/test'
import { config } from './types.js'

type ActorSession = {
  context: BrowserContext
  page: Page
}

export class AppWorld extends World {
  browser!: Browser
  private actorSessions = new Map<string, ActorSession>()
  private currentActor = 'default'
  candidateName?: string
  candidateEmail?: string
  candidateId?: string

  async openBrowser() {
    const headed = process.env.HEADED === '1' || process.env.HEADED === 'true'
    this.browser = await chromium.launch({
      headless: !headed,
      slowMo: headed ? 150 : 0,
    })
    await this.useActor('default')
  }

  async closeBrowser() {
    for (const { context } of this.actorSessions.values()) {
      await context.close().catch(() => {})
    }
    this.actorSessions.clear()
    await this.browser?.close()
  }

  get context() {
    return this.getActorSession(this.currentActor).context
  }

  get page() {
    return this.getActorSession(this.currentActor).page
  }

  get activeActor() {
    return this.currentActor
  }

  async useActor(name: string) {
    if (!this.actorSessions.has(name)) {
      const context = await this.browser.newContext({
        locale: process.env.E2E_LOCALE ?? 'en-US',
      })
      const page = await context.newPage()
      page.setDefaultTimeout(config.responseTimeout)
      this.actorSessions.set(name, { context, page })
    }
    this.currentActor = name
  }

  setActorPage(name: string, page: Page) {
    const session = this.getActorSession(name)
    page.setDefaultTimeout(config.responseTimeout)
    this.actorSessions.set(name, { ...session, page })
  }

  private getActorSession(name: string) {
    const session = this.actorSessions.get(name)
    if (!session) {
      throw new Error(`Actor session "${name}" has not been initialized`)
    }
    return session
  }

  async gotoApp(path: string) {
    const url = path.startsWith('http')
      ? path
      : `${config.appUrl}${path.startsWith('/') ? path : `/${path}`}`
    await this.page.goto(url, { waitUntil: 'domcontentloaded' })
    await this.page.waitForLoadState('networkidle', { timeout: 2_500 }).catch(() => {})
    await this.page.waitForTimeout(400)
  }

  async login(email: string, password: string) {
    const startingUrl = this.page.url()
    await this.page.locator('input[name="email"]').fill(email)
    await this.page.locator('input[name="password"]').fill(password)
    await this.page.getByRole('button', { name: /^login$/i }).click({
      noWaitAfter: true,
    })
    await Promise.race([
      this.page.waitForURL(
        (url) => url.toString() !== startingUrl && !url.toString().includes('/auth/login'),
        { timeout: 5_000 },
      ),
      this.page
        .getByText(/invalid email or password/i, { exact: false })
        .first()
        .waitFor({ state: 'visible', timeout: 5_000 }),
    ]).catch(() => {})
    await this.page.waitForLoadState('networkidle', { timeout: 3_000 }).catch(() => {})
    await this.page.waitForTimeout(400)

    const invalidCredentials = await this.page
      .getByText(/invalid email or password/i, { exact: false })
      .first()
      .isVisible()
      .catch(() => false)

    if (invalidCredentials) {
      throw new Error(`Login failed for ${email}: invalid email or password`)
    }
  }

  async expectText(text: string) {
    const locator = this.page.getByText(text, { exact: false }).first()
    try {
      await locator.waitFor({ state: 'visible', timeout: config.responseTimeout })
    } catch (error) {
      const bodyText = (await this.page.locator('body').innerText().catch(() => ''))
        .replace(/\s+/g, ' ')
        .slice(0, 1000)
      throw new Error(
        `Expected to see "${text}" but it was not visible. Body excerpt: ${bodyText}`,
        { cause: error },
      )
    }
  }
}

setWorldConstructor(AppWorld)
