import { Given, When, Then } from '@cucumber/cucumber'
import { expect } from '@playwright/test'
import type { AppWorld } from '../support/world.js'

Given('I visit {string}', async function (this: AppWorld, path: string) {
  await this.gotoApp(path)
})

Given(
  'I am logged in as {string} with password {string}',
  async function (this: AppWorld, email: string, password: string) {
    await this.login(email, password)
  },
)

When('I log in as {string} with password {string}', async function (
  this: AppWorld,
  email: string,
  password: string,
) {
  await this.login(email, password)
})

When('I log out', async function (this: AppWorld) {
  await this.logout()
})

const looksLikeCss = (s: string) => /[\[#.>:]/.test(s) || s.startsWith('input')
const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')

async function field(world: AppWorld, name: string) {
  if (looksLikeCss(name)) return world.page.locator(name).first()
  const byId = world.page.locator(`[data-testid="${name}"]`).first()
  try {
    await byId.waitFor({ state: 'attached', timeout: 5000 })
    const inner = byId.locator('input, textarea, select').first()
    if (await inner.count()) return inner
    return byId
  } catch {
    // fall through
  }
  const byLabel = world.page.getByLabel(name, { exact: false })
  if (await byLabel.count()) return byLabel.first()
  return world.page.getByPlaceholder(name, { exact: false }).first()
}

async function isVisibleWithin(
  target: ReturnType<AppWorld['page']['locator']>,
  timeout = 5000,
) {
  try {
    await target.waitFor({ state: 'visible', timeout })
    return true
  } catch {
    return false
  }
}

async function activate(target: ReturnType<AppWorld['page']['locator']>, timeout = 5000) {
  try {
    await target.click({ timeout })
    return
  } catch {
    await target.evaluate((node) => {
      ;(node as HTMLElement).click()
    })
  }
}

async function settleAfterClick(world: AppWorld, previousUrl?: string) {
  if (previousUrl) {
    await world.page
      .waitForURL((url) => url.toString() !== previousUrl, { timeout: 3000 })
      .catch(() => {})
  }
  await world.page.waitForLoadState('networkidle', { timeout: 1000 }).catch(() => {})
  await world.page.waitForTimeout(750)
}

async function clickAndConfirmIfNeeded(world: AppWorld, target: ReturnType<AppWorld['page']['locator']>) {
  await activate(target)
  await settleAfterClick(world)

  if (
    await isVisibleWithin(
      world.page.getByRole('button', { name: /bestätigen\?|confirm\?/i }).first(),
      1000,
    )
  ) {
    await activate(
      world.page.getByRole('button', { name: /bestätigen\?|confirm\?/i }).first(),
      1000,
    )
    await world.page.waitForLoadState('networkidle', { timeout: 1000 }).catch(() => {})
    await world.page.waitForTimeout(250)
    return
  }
  if (
    await isVisibleWithin(
      world.page.getByText(/bestätigen\?|confirm\?/i, { exact: false }).first(),
      1000,
    )
  ) {
    await activate(
      world.page.getByText(/bestätigen\?|confirm\?/i, { exact: false }).first(),
      1000,
    )
    await world.page.waitForLoadState('networkidle', { timeout: 1000 }).catch(() => {})
    await world.page.waitForTimeout(250)
  }
}

When(
  'I fill {string} with {string}',
  async function (this: AppWorld, name: string, value: string) {
    const target = await field(this, name)
    await target.fill('')
    await target.fill(value)
  },
)

When('I check {string}', async function (this: AppWorld, name: string) {
  const target = await field(this, name)
  await target.check()
})

When('I uncheck {string}', async function (this: AppWorld, name: string) {
  const target = await field(this, name)
  await target.uncheck()
})

When(
  'I select {string} from {string}',
  async function (this: AppWorld, value: string, name: string) {
    const target = await field(this, name)
    const tag = await target
      .evaluate((el) => (el as HTMLElement).tagName.toLowerCase())
      .catch(() => '')
    if (tag === 'select') {
      await target.selectOption({ label: value })
      return
    }
    await target.click()
    await this.page.getByRole('option', { name: value, exact: false }).first().click()
  },
)

Then(
  'in row {string} I do not see {string}',
  async function (this: AppWorld, rowText: string, text: string) {
    const row = this.page.getByRole('row', { name: new RegExp(rowText) })
    await expect(row.getByText(text, { exact: false }).first()).toBeHidden()
  },
)

When(
  'in row {string} I click {string}',
  async function (this: AppWorld, rowText: string, label: string) {
    const row = this.page.getByRole('row', { name: new RegExp(rowText) })
    const roles = ['button', 'link', 'menuitem'] as const
    const previousUrl = this.page.url()
    for (const role of roles) {
      const candidate = row.getByRole(role, { name: new RegExp(label, 'i') }).first()
      if (await isVisibleWithin(candidate)) {
        await activate(candidate)
        await settleAfterClick(this, previousUrl)
        return
      }
    }
    const byText = row.getByText(label, { exact: false }).first()
    if (await isVisibleWithin(byText)) {
      await activate(byText)
      await settleAfterClick(this, previousUrl)
      return
    }
    throw new Error(`Could not click "${label}" inside row "${rowText}"`)
  },
)

When('I click {string}', async function (this: AppWorld, label: string) {
  const byId = this.page.getByTestId(label).first()
  if (await isVisibleWithin(byId)) {
    await clickAndConfirmIfNeeded(this, byId)
    return
  }
  const fuzzyName = new RegExp(escapeRegex(label), 'i')
  const roles = ['button', 'tab', 'link', 'menuitem'] as const
  for (const role of roles) {
    const candidate = this.page.getByRole(role, { name: fuzzyName }).first()
    if (await isVisibleWithin(candidate)) {
      await clickAndConfirmIfNeeded(this, candidate)
      return
    }
  }
  const byText = this.page.getByText(label, { exact: false }).first()
  if (await isVisibleWithin(byText)) {
    await clickAndConfirmIfNeeded(this, byText)
    return
  }
  throw new Error(`Could not click "${label}"`)
})

When('I accept the next confirmation', async function (this: AppWorld) {
  this.page.once('dialog', (d) => void d.accept())
})

Then('I see {string}', async function (this: AppWorld, text: string) {
  await this.expectText(text)
})

Then(
  'the field {string} has value {string}',
  async function (this: AppWorld, name: string, value: string) {
    const target = await field(this, name)
    await expect(target).toHaveValue(value)
  },
)

Then('I do not see {string}', async function (this: AppWorld, text: string) {
  await expect(
    this.page.getByText(text, { exact: false }).first(),
  ).toBeHidden()
})

Then('the URL contains {string}', async function (this: AppWorld, fragment: string) {
  await this.page.waitForURL((url) => url.toString().includes(fragment))
})

Then(
  'the URL does not contain {string}',
  async function (this: AppWorld, fragment: string) {
    await this.page.waitForURL((url) => !url.toString().includes(fragment))
  },
)
