import { Given, Then, When } from '@cucumber/cucumber'
import { expect, type Locator } from '@playwright/test'
import { dirname, resolve } from 'path'
import { fileURLToPath } from 'url'
import type { AppWorld } from '../support/world.js'

const looksLikeCss = (s: string) =>
  s.startsWith('#') ||
  s.startsWith('.') ||
  s.startsWith('[') ||
  s.startsWith('input') ||
  s.includes('>') ||
  s.includes(':')
const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const __dirname = dirname(fileURLToPath(import.meta.url))
const repoRoot = resolve(__dirname, '../../..')
const uploadFixturePath = resolve(repoRoot, 'apps/website/public/favicon-16x16.png')

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()
  if (await byId.count()) {
    const inner = byId.locator('input, textarea, select').first()
    if (await inner.count()) return inner
    return byId
  }
  const byLabel = world.page.getByLabel(name, { exact: false })
  if (await byLabel.count()) return firstVisible(byLabel)
  const byPlaceholder = world.page.getByPlaceholder(name, { exact: false })
  if (await byPlaceholder.count()) return firstVisible(byPlaceholder)
  return world.page.getByText(name, { exact: false }).first()
}

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

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

async function firstVisible(target: ReturnType<AppWorld['page']['locator']>) {
  return await nthVisible(target, 0)
}

async function nthVisible(
  target: ReturnType<AppWorld['page']['locator']>,
  visibleIndex: number,
) {
  const count = await target.count()
  let seenVisible = 0
  for (let index = 0; index < count; index += 1) {
    const candidate = target.nth(index)
    if (await candidate.isVisible().catch(() => false)) {
      if (seenVisible === visibleIndex) {
        return candidate
      }
      seenVisible += 1
    }
  }
  return target.first()
}

async function visibleOption(world: AppWorld, name: string, visibleIndex = 0) {
  return nthVisible(
    world.page.getByRole('option', {
      name: new RegExp(`^${escapeRegex(name)}$`, 'i'),
    }),
    visibleIndex,
  )
}

async function settle(world: AppWorld) {
  await world.page.waitForLoadState('networkidle', { timeout: 2_000 }).catch(() => {})
  await world.page.waitForTimeout(250)
}

async function useActor(world: AppWorld, actor: string) {
  await world.useActor(actor)
}

async function labeledComboboxControl(world: AppWorld, label: string) {
  const name = new RegExp(escapeRegex(label), 'i')
  const byCombobox = world.page.getByRole('combobox', { name })
  if (await byCombobox.count()) {
    return firstVisible(byCombobox)
  }

  const byTextbox = world.page.getByRole('textbox', { name })
  if (await byTextbox.count()) {
    return firstVisible(byTextbox)
  }

  return firstVisible(world.page.getByLabel(label, { exact: false }))
}

async function selectComboboxOption(
  world: AppWorld,
  label: string,
  search: string,
  option = search,
) {
  const input = await labeledComboboxControl(world, label)
  let lastVisibleOptions: string[] = []
  let lastListId = ''
  const isReadonly = await input
    .evaluate((node) => node instanceof HTMLInputElement && node.readOnly)
    .catch(() => false)

  await expect(input).toBeEnabled({ timeout: 5_000 })
  await input.click()

  if (!isReadonly) {
    await input.fill('')
    await input.fill(search)
    await world.page.waitForTimeout(150)
  }

  lastListId = (await input.getAttribute('aria-controls').catch(() => '')) ?? ''
  if (!lastListId) {
    await input.press('ArrowDown').catch(() => {})
    await world.page.waitForTimeout(150)
    lastListId = (await input.getAttribute('aria-controls').catch(() => '')) ?? ''
  }

  const optionsRoot = lastListId ? world.page.locator(`#${lastListId}`) : world.page
  lastVisibleOptions = await optionsRoot
    .locator('[role="option"]')
    .evaluateAll((nodes) =>
      nodes
        .map((node) => (node as HTMLElement).innerText.trim())
        .filter((value): value is string => Boolean(value))
        .slice(0, 15),
    )
    .catch(() => [])
  const optionLocator = lastListId
    ? optionsRoot
        .getByRole('option', {
          name: new RegExp(`^${escapeRegex(option)}$`, 'i'),
        })
        .first()
    : await visibleOption(world, option)
  const fuzzyOptionLocator = optionsRoot
    .locator('[role="option"]')
    .filter({ hasText: new RegExp(escapeRegex(search), 'i') })
    .first()

  if (await isVisibleWithin(optionLocator, 3_000)) {
    await optionLocator.click()
    await settle(world)
    if (!isReadonly || (await input.inputValue().catch(() => '')).trim()) {
      return
    }
  }

  if (await isVisibleWithin(fuzzyOptionLocator, 1_500)) {
    await fuzzyOptionLocator.click()
    await settle(world)
    if (!isReadonly || (await input.inputValue().catch(() => '')).trim()) {
      return
    }
  }

  await input.press('ArrowDown')
  await input.press('Enter')
  await settle(world)

  if ((await input.inputValue().catch(() => '')).trim()) {
    return
  }

  throw new Error(
    `Could not select option "${option}" from "${label}". Listbox: ${lastListId || 'none'}. Visible options after typing: ${lastVisibleOptions.join(' | ') || 'none'}`,
  )
}

async function selectCountryOption(world: AppWorld, label: string, country: string) {
  const input = await labeledComboboxControl(world, label)
  let lastVisibleOptions: string[] = []
  let lastListId = ''

  await expect(input).toBeEnabled({ timeout: 5_000 })

  for (let attempt = 0; attempt < 2; attempt += 1) {
    await input.click()
    await input.fill('')
    await input.fill(country)
    await world.page.waitForTimeout(150)

    lastListId = (await input.getAttribute('aria-controls').catch(() => '')) ?? ''
    if (!lastListId) {
      await input.press('ArrowDown').catch(() => {})
      await world.page.waitForTimeout(150)
      lastListId = (await input.getAttribute('aria-controls').catch(() => '')) ?? ''
    }

    const optionsRoot = lastListId ? world.page.locator(`#${lastListId}`) : world.page
    lastVisibleOptions = await optionsRoot
      .locator('[role="option"]')
      .evaluateAll((nodes) =>
        nodes
          .map((node) => (node as HTMLElement).innerText.trim())
          .filter((value): value is string => Boolean(value))
          .slice(0, 20),
      )
      .catch(() => [])

    const optionLocator = lastListId
      ? optionsRoot
          .getByRole('option', {
            name: new RegExp(`^${escapeRegex(country)}$`, 'i'),
          })
          .first()
      : await visibleOption(world, country)

    await optionLocator.waitFor({ state: 'visible', timeout: 5_000 }).catch(() => {})
    if (await optionLocator.isVisible().catch(() => false)) {
      await optionLocator.click()
    } else {
      await input.press('ArrowDown').catch(() => {})
      await input.press('Enter').catch(() => {})
    }

    await input.blur().catch(() => {})
    await settle(world)

    if ((await input.inputValue().catch(() => '')).trim().toLowerCase() === country.toLowerCase()) {
      return
    }
  }

  throw new Error(
    `Could not select country "${country}" from "${label}". Listbox: ${lastListId || 'none'}. Visible options after typing: ${lastVisibleOptions.join(' | ') || 'none'}`,
  )
}

async function chooseOptionFromControl(
  world: AppWorld,
  control: Locator,
  option: string,
) {
  await control.click({ force: true })

  const tagName = await control.evaluate((node) => node.tagName.toLowerCase())
  if (tagName === 'input') {
    await control.fill('').catch(() => {})
    await control.fill(option).catch(() => {})
  }

  const optionLocator = await visibleOption(world, option)

  if (await isVisibleWithin(optionLocator, 1_500)) {
    await optionLocator.click()
    await settle(world)
    return true
  }

  return false
}

async function setContainerValue(
  world: AppWorld,
  container: Locator,
  value: string,
) {
  const control = await firstVisible(container.locator('input, textarea, button'))
  await control.scrollIntoViewIfNeeded().catch(() => {})

  if (await chooseOptionFromControl(world, control, value)) {
    return
  }

  const tagName = await control.evaluate((node) => node.tagName.toLowerCase())
  if (tagName === 'input' || tagName === 'textarea') {
    await control.fill('').catch(() => {})
    await control.fill(value)
    await control.blur().catch(() => {})
    await world.page.waitForTimeout(700)
    await settle(world)
    return
  }

  throw new Error(`Could not set value "${value}" in the selected control`)
}

async function uploadCurrentStepFile(world: AppWorld, filePath = uploadFixturePath) {
  const input = world.page.locator('input[type="file"]').first()
  await input.setInputFiles(filePath)
  await world.page.getByText('favicon-16x16.png', { exact: false }).last().waitFor({
    state: 'visible',
  })
  await settle(world)
}

async function isUploadStepVisible(world: AppWorld) {
  return isVisibleWithin(world.page.getByRole('button', { name: /^Choose$/i }).first(), 1_500)
}

async function advanceToUploadStep(world: AppWorld) {
  await clickPrimaryStepButton(world, 'Next')

  if (await isUploadStepVisible(world)) {
    return
  }

  const stateErrorVisible = await world.page
    .getByText('Please select at least one state.', { exact: false })
    .first()
    .isVisible()
    .catch(() => false)

  if (stateErrorVisible) {
    await selectComboboxOption(world, 'Where would you like to work in Germany?', 'Berlin')
    await world.page.keyboard.press('Escape').catch(() => {})
    await settle(world)
    await clickPrimaryStepButton(world, 'Next')
  }

  await expectAdvancedToUploadStep(world)
}

async function expectAdvancedToUploadStep(world: AppWorld) {
  if (await isUploadStepVisible(world)) {
    return
  }

  const visibleMessages = (
    await Promise.all([
      'Please select at least one state.',
      'Please select an option.',
      'You must select a valid country.',
      'jobs.basicinfo.validation.dateofbirthrequired',
    ].map(async (message) =>
      (await world.page.getByText(message, { exact: false }).first().isVisible().catch(() => false))
        ? message
        : null
    ))
  ).filter(Boolean)

  const workLocationInput = world.page
    .getByLabel('Where would you like to work in Germany?', { exact: false })
    .first()
  const dateInput = world.page.locator('input[data-dates-input="true"]').first()
  const countryInput = world.page
    .getByLabel('In which country did you finish your nursing education?', { exact: false })
    .first()
  const selectedStates = await world.page
    .locator('[data-combobox-pill]')
    .evaluateAll((nodes) =>
      nodes
        .map((node) => node.textContent?.trim() ?? '')
        .filter(Boolean),
    )
    .catch(() => [])
  const radioStates = {
    yes: await Promise.all(
      [0, 1, 2].map((index) =>
        world.page
          .getByRole('radio', { name: /^Yes$/i })
          .nth(index)
          .isChecked()
          .catch(() => null),
      ),
    ),
    no: await Promise.all(
      [0, 1, 2].map((index) =>
        world.page
          .getByRole('radio', { name: /^No$/i })
          .nth(index)
          .isChecked()
          .catch(() => null),
      ),
    ),
  }
  const controlState = {
    url: world.page.url(),
    workLocationValue: await workLocationInput.inputValue().catch(() => ''),
    selectedStates,
    dateValue: await dateInput.inputValue().catch(() => ''),
    countryValue: await countryInput.inputValue().catch(() => ''),
    radioStates,
  }
  const bodyText = (await world.page.locator('body').innerText()).replace(/\s+/g, ' ').slice(0, 500)

  throw new Error(
    `Application did not advance to the upload step. Visible validation messages: ${visibleMessages.join(' | ') || 'none'}. Control state: ${JSON.stringify(controlState)}. Body excerpt: ${bodyText}`,
  )
}

async function selectVisibleRadioByName(
  world: AppWorld,
  name: string,
  visibleIndex: number,
) {
  const radio = await nthVisible(world.page.getByRole('radio', { name: new RegExp(`^${escapeRegex(name)}$`, 'i') }), visibleIndex)
  const id = await radio.getAttribute('id')
  if (id) {
    await world.page.locator(`label[for="${id}"]`).click()
    return
  }
  await radio.click({ force: true })
}

async function clickPrimaryStepButton(world: AppWorld, label: 'Next' | 'Submit') {
  await world.page.getByRole('button', { name: new RegExp(`^${label}$`, 'i') }).click()
  await settle(world)
}

async function startCandidateApplication(world: AppWorld) {
  const suffix = Date.now().toString(36)
  world.candidateName = `E2E Candidate ${suffix}`
  world.candidateEmail = `e2e-candidate-${suffix}@heygermany.test`

  await world.page.getByLabel('Full Name', { exact: false }).fill(world.candidateName)
  await world.page.getByLabel('E-Mail', { exact: false }).fill(world.candidateEmail)
  await world.page.locator('#terms').check()
  await world.page.getByRole('button', { name: /^Check My Qualification$/i }).click()
  await world.page.waitForURL((url) => url.toString().includes('/jobs/application'))
  await settle(world)
}

async function completeCandidateApplication(world: AppWorld) {
  await selectComboboxOption(world, 'Where would you like to work in Germany?', 'Berlin')
  await world.page.keyboard.press('Escape').catch(() => {})
  await settle(world)

  await selectVisibleRadioByName(world, 'Yes', 0)
  await selectVisibleRadioByName(world, 'No', 1)
  await selectVisibleRadioByName(world, 'No', 2)

  const dateInput = await firstVisible(world.page.locator('input[data-dates-input="true"]'))
  await dateInput.fill('01/15/1990')

  await selectCountryOption(
    world,
    'In which country did you finish your nursing education?',
    'Philippines',
  )
  await advanceToUploadStep(world)

  await uploadCurrentStepFile(world)
  await clickPrimaryStepButton(world, 'Next')

  await world.page.locator('#license-not-provided').check()
  await clickPrimaryStepButton(world, 'Next')

  await world.page.getByText("I don't have a language certificate yet.", { exact: false }).click()
  await selectComboboxOption(world, 'Level', 'B1')
  await clickPrimaryStepButton(world, 'Next')

  const submitButton = world.page.getByRole('button', {
    name: /^Submit(?: Application)?$/i,
  })
  if (!(await isVisibleWithin(submitButton, 5_000))) {
    const visibleButtons = await world.page
      .getByRole('button')
      .evaluateAll((nodes) =>
        nodes
          .map((node) => (node as HTMLElement).innerText.trim())
          .filter(Boolean),
      )
      .catch(() => [])
    const visibleHeadings = await world.page
      .locator('h1, h2, h3')
      .evaluateAll((nodes) =>
        nodes
          .map((node) => (node as HTMLElement).innerText.trim())
          .filter(Boolean),
      )
      .catch(() => [])
    const levelValue = await world.page
      .getByRole('textbox', { name: /Level/i })
      .first()
      .inputValue()
      .catch(() => '')
    const bodyText = (await world.page.locator('body').innerText()).replace(/\s+/g, ' ').slice(0, 600)

    throw new Error(
      `Application did not reach the final submit state. Level value: ${levelValue || 'empty'}. Visible headings: ${visibleHeadings.join(' | ') || 'none'}. Visible buttons: ${visibleButtons.join(' | ') || 'none'}. Body excerpt: ${bodyText}`,
    )
  }

  await submitButton.click()
  await settle(world)
}

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

Given(
  'the {string} actor visits {string}',
  async function (this: AppWorld, actor: string, path: string) {
    await useActor(this, actor)
    await this.gotoApp(path)
  },
)

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

When(
  'the {string} actor logs in as {string} with password {string}',
  async function (this: AppWorld, actor: string, email: string, password: string) {
    await useActor(this, actor)
    await this.login(email, password)
  },
)

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

When('I wait for {int} milliseconds', async function (this: AppWorld, ms: number) {
  await this.page.waitForTimeout(ms)
})

When('the {string} actor starts a new application', async function (this: AppWorld, actor: string) {
  await useActor(this, actor)
  await startCandidateApplication(this)
})

When(
  'the {string} actor completes and submits the candidate application',
  async function (this: AppWorld, actor: string) {
    await useActor(this, actor)
    await completeCandidateApplication(this)
  },
)

When('the {string} actor logs into backoffice', async function (this: AppWorld, actor: string) {
  await useActor(this, actor)
  await this.gotoApp('/en/backoffice/auth/login')
  await this.login('e2e-backoffice-login@heygermany.test', 'BackofficePass123!')
})

When('the {string} actor logs into company', async function (this: AppWorld, actor: string) {
  await useActor(this, actor)
  await this.gotoApp('/en/company/auth/login')
  await this.login('e2e-company-login@heygermany.test', 'CompanyPass123!')
})

When(
  'the {string} actor opens the current candidate in backoffice',
  async function (this: AppWorld, actor: string) {
    if (!this.candidateName) {
      throw new Error('No candidate name is available for the current scenario')
    }

    await useActor(this, actor)
    const row = this.page.locator('tr', { hasText: this.candidateName }).first()
    await row.waitFor({ state: 'visible' })

    const popupPromise = this.context.waitForEvent('page')
    await row.click()
    const popup = await popupPromise
    await popup.waitForLoadState('domcontentloaded')
    await popup.waitForLoadState('networkidle', { timeout: 2_000 }).catch(() => {})
    this.setActorPage(actor, popup)
    const candidateId = popup.url().match(/\/candidate\/([^/?#]+)/)?.[1]
    if (candidateId) {
      this.candidateId = candidateId
    }
  },
)

When(
  'the {string} actor opens the current candidate in company',
  async function (this: AppWorld, actor: string) {
    if (!this.candidateName) {
      throw new Error('No candidate name is available for the current scenario')
    }

    await useActor(this, actor)
    await this.gotoApp('/en/company/candidates')
    const link = this.page.locator('a', { hasText: this.candidateName }).first()
    await link.waitFor({ state: 'visible' })
    await link.click()
    await settle(this)
    const candidateId = this.page.url().match(/\/candidate\/([^/?#]+)/)?.[1]
    if (candidateId) {
      this.candidateId = candidateId
    }
  },
)

When(
  'the {string} actor marks the candidate as {string}',
  async function (this: AppWorld, actor: string, status: string) {
    await useActor(this, actor)
    const combobox = await firstVisible(
      this.page
        .locator('input[placeholder="Select status"], button[title="Select status"]')
        .or(this.page.getByRole('combobox'))
    )
    await combobox.click()
    const option = this.page.getByRole('option', {
      name: new RegExp(`^${escapeRegex(status)}$`, 'i'),
    }).first()
    await option.waitFor({ state: 'visible' })
    await option.click()
    await settle(this)
  },
)

When(
  'the {string} actor sets the backoffice field {string} to {string}',
  async function (this: AppWorld, actor: string, label: string, value: string) {
    await useActor(this, actor)
    const labelText = this.page.getByText(label, { exact: false }).first()
    const labeledContainer = labelText.locator(
      'xpath=ancestor::div[.//input or .//button][1]',
    )

    if (await labeledContainer.count()) {
      try {
        await setContainerValue(this, labeledContainer, value)
        return
      } catch {
        // fall back to direct field lookup below
      }
    }

    const target = await field(this, label)
    await target.scrollIntoViewIfNeeded().catch(() => {})

    if (await chooseOptionFromControl(this, target, value)) {
      return
    }

    const tagName = await target.evaluate((node) => node.tagName.toLowerCase()).catch(() => '')
    if (tagName === 'input' || tagName === 'textarea') {
      await target.fill('').catch(() => {})
      await target.fill(value).catch(() => {})
      await target.blur().catch(() => {})
      await this.page.waitForTimeout(700)
      await settle(this)
      return
    }

    throw new Error(`Could not set backoffice field "${label}" to "${value}"`)
  },
)

When(
  'the {string} actor sets the verification field {string} to {string}',
  async function (this: AppWorld, actor: string, fieldName: string, value: string) {
    await useActor(this, actor)
    const row = this.page.locator('tr', { hasText: fieldName }).first()
    await row.waitFor({ state: 'visible' })
    await setContainerValue(this, row.locator('td').last(), value)
  },
)

When(
  'the {string} actor revisits the current candidate application',
  async function (this: AppWorld, actor: string) {
    await useActor(this, actor)
    await this.gotoApp('/en/jobs/application')
  },
)

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 click {string}', async function (this: AppWorld, label: string) {
  const byId = this.page.getByTestId(label).first()
  if (await isVisibleWithin(byId, 1_000)) {
    await activate(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, 1_000)) {
      await activate(candidate)
      return
    }
  }

  const byText = this.page.getByText(label, { exact: false }).first()
  if (await isVisibleWithin(byText, 1_000)) {
    await activate(byText)
    return
  }

  throw new Error(`Could not click "${label}"`)
})

When('the {string} actor clicks {string}', async function (this: AppWorld, actor: string, label: string) {
  await useActor(this, actor)
  const byId = this.page.getByTestId(label).first()
  if (await isVisibleWithin(byId, 1_000)) {
    await activate(byId)
    await settle(this)
    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, 1_000)) {
      await activate(candidate)
      await settle(this)
      return
    }
  }

  const byText = this.page.getByText(label, { exact: false }).first()
  if (await isVisibleWithin(byText, 1_000)) {
    await activate(byText)
    await settle(this)
    return
  }

  throw new Error(`Could not click "${label}"`)
})

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

Then('the {string} actor sees {string}', async function (this: AppWorld, actor: string, text: string) {
  await useActor(this, actor)
  await this.expectText(text)
})

Then(
  'the {string} actor sees the current candidate name',
  async function (this: AppWorld, actor: string) {
    if (!this.candidateName) {
      throw new Error('No candidate name is available for the current scenario')
    }

    await useActor(this, actor)
    await this.expectText(this.candidateName)
  },
)

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))
  },
)

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