import type { Page } from '@playwright/test'
import type { DifyWorld } from '../../support/world'
import { Given, Then, When } from '@cucumber/cucumber'
import { expect } from '@playwright/test'
import { waitForAgentConfigureAutosaved } from '../../../support/agent-configure'
import {
  createConfiguredTestAgent,
  createTestAgent,
  getAgentConfigurePath,
  saveAgentComposerDraft,
} from '../../agent-v2/support/agent'
import { getAgentDriveSkills, uploadAgentDriveSkill } from '../../agent-v2/support/agent-drive'
import {
  concurrentFirstAgentPrompt,
  concurrentSecondAgentPrompt,
  createAgentSoulConfigWithModel,
  createPublishableAgentSoulConfig,
  normalAgentPrompt,
  normalAgentSoulConfig,
  updatedAgentPrompt,
} from '../../agent-v2/support/agent-soul'
import {
  agentBuilderTestMaterials,
  getAgentBuilderTestMaterialPath,
} from '../../agent-v2/support/test-materials'
import {
  expectNormalAgentPromptDraft,
  getCurrentAgentId,
  getPreseededAgent,
} from './configure-helpers'

const concurrentAgentPrompts = [concurrentFirstAgentPrompt, concurrentSecondAgentPrompt]

const getPromptEditor = (page: Page) =>
  page.getByRole('region', { name: 'Prompt' }).getByRole('textbox', { name: 'Prompt' })

async function fillAgentPromptEditor(page: Page, prompt: string) {
  const promptSection = page.getByRole('region', { name: 'Prompt' })

  await expect(promptSection).toBeVisible({ timeout: 30_000 })
  await getPromptEditor(page).fill(prompt)
}

async function selectAgentModel(page: Page, modelName: string) {
  await page.getByRole('combobox').first().click()
  await page.getByPlaceholder('Search model').fill(modelName)
  const escapedModelName = modelName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  await page.getByRole('option', { name: new RegExp(`${escapedModelName}(?:\\s|$)`) }).click()
}

async function expectAgentComposerPrompt(world: DifyWorld, agentId: string, prompt: string) {
  await expect
    .poll(
      async () => {
        const draft = await world
          .getConsoleClient()
          .agent.byAgentId.composer.get({ params: { agent_id: agentId } })
        return draft.agent_soul?.prompt?.system_prompt
      },
      { timeout: 30_000 },
    )
    .toBe(prompt)
}

Given('an Agent v2 test agent has been created via API', async function (this: DifyWorld) {
  const agent = await createTestAgent(this.getConsoleClient())
  this.createdAgentIds.push(agent.id)
  this.lastCreatedAgentName = agent.name
  this.lastCreatedAgentRole = agent.role ?? undefined
})

Given(
  'a basic configured Agent v2 test agent has been created via API',
  async function (this: DifyWorld) {
    const agent = await createConfiguredTestAgent(this.getConsoleClient())
    this.createdAgentIds.push(agent.id)
    this.lastCreatedAgentName = agent.name
    this.lastCreatedAgentRole = agent.role ?? undefined
  },
)

Given('a runnable Agent v2 test agent has been created via API', async function (this: DifyWorld) {
  if (!this.agentBuilder.fixtures.stableModel)
    throw new Error('Create a runnable Agent v2 test agent after stable model fixture setup.')

  const agent = await createConfiguredTestAgent(this.getConsoleClient(), {
    agentSoul: createAgentSoulConfigWithModel(
      normalAgentSoulConfig,
      this.agentBuilder.fixtures.stableModel,
    ),
  })
  this.createdAgentIds.push(agent.id)
  this.lastCreatedAgentName = agent.name
  this.lastCreatedAgentRole = agent.role ?? undefined
})

Given(
  'a runnable Agent v2 test agent using the agent-decision model has been created via API',
  async function (this: DifyWorld) {
    if (!this.agentBuilder.fixtures.agentDecisionModel) {
      throw new Error(
        'Create an agent-decision Agent v2 test agent after agent-decision model fixture setup.',
      )
    }

    const agent = await createConfiguredTestAgent(this.getConsoleClient(), {
      agentSoul: createAgentSoulConfigWithModel(
        normalAgentSoulConfig,
        this.agentBuilder.fixtures.agentDecisionModel,
      ),
    })
    this.createdAgentIds.push(agent.id)
    this.lastCreatedAgentName = agent.name
    this.lastCreatedAgentRole = agent.role ?? undefined
  },
)

Given('a minimal Agent v2 composer draft has been synced', async function (this: DifyWorld) {
  const agentId = getCurrentAgentId(this)

  await saveAgentComposerDraft(this.getConsoleClient(), agentId)
})

Given('the Agent v2 composer draft uses the normal E2E prompt', async function (this: DifyWorld) {
  await saveAgentComposerDraft(
    this.getConsoleClient(),
    getCurrentAgentId(this),
    normalAgentSoulConfig,
  )
})

Given('the Agent v2 composer draft is publishable', async function (this: DifyWorld) {
  await saveAgentComposerDraft(
    this.getConsoleClient(),
    getCurrentAgentId(this),
    createPublishableAgentSoulConfig(normalAgentSoulConfig),
  )
})

Given(
  'the e2e-summary-skill Skill is available to the Agent v2 test agent',
  async function (this: DifyWorld) {
    const agentId = getCurrentAgentId(this)
    const upload = await uploadAgentDriveSkill(this.getConsoleClient(), {
      agentId,
      fileName: agentBuilderTestMaterials.summarySkill,
      filePath: getAgentBuilderTestMaterialPath('summarySkill'),
    })
    this.createdAgentDriveFiles.push({ agentId, key: upload.skill.skill_md_key })
    if (upload.skill.archive_key)
      this.createdAgentDriveFiles.push({ agentId, key: upload.skill.archive_key })
  },
)

Then(
  'the Agent v2 test agent should include drive skill {string}',
  async function (this: DifyWorld, skillName: string) {
    const skills = await getAgentDriveSkills(this.getConsoleClient(), getCurrentAgentId(this))

    expect(skills.map((skill) => skill.name)).toContain(skillName)
  },
)

When('I open the Agent v2 configure page', async function (this: DifyWorld) {
  await this.getPage().goto(getAgentConfigurePath(getCurrentAgentId(this)))
})

When(
  'I select the stable E2E model in the Agent v2 model selector',
  async function (this: DifyWorld) {
    const stableModel = this.agentBuilder.fixtures.stableModel
    if (!stableModel)
      throw new Error('Stable chat model fixture setup must run before selecting the Agent model.')

    await selectAgentModel(this.getPage(), stableModel.name)
  },
)

When(
  'I select the agent-decision E2E model in the Agent v2 model selector',
  async function (this: DifyWorld) {
    const model = this.agentBuilder.fixtures.agentDecisionModel
    if (!model)
      throw new Error(
        'Agent-decision chat model fixture setup must run before selecting the Agent model.',
      )

    await selectAgentModel(this.getPage(), model.name)
  },
)

When('I switch to the Agent v2 Configure section', async function (this: DifyWorld) {
  const page = this.getPage()
  const agentId = getCurrentAgentId(this)

  await page.getByRole('link', { name: 'Configure' }).click()
  await expect(page).toHaveURL(new RegExp(`/agents/${agentId}/configure(?:\\?.*)?$`))
  await expect(page.getByRole('heading', { name: 'Configure' })).toBeVisible({ timeout: 30_000 })
})

When(
  'I leave the Agent v2 configure page immediately after editing',
  async function (this: DifyWorld) {
    const page = this.getPage()

    await page.goto('/agents')
    await expect(page).toHaveURL(/\/agents(?:\?.*)?$/)
  },
)

When('I open the Agent v2 configure page from the Agent Roster', async function (this: DifyWorld) {
  const page = this.getPage()
  const agentId = getCurrentAgentId(this)
  const agentName = this.lastCreatedAgentName
  if (!agentName) throw new Error('No Agent v2 name found. Create an Agent v2 test agent first.')

  await page.goto('/agents')
  await page.getByRole('link', { name: agentName }).click()
  await expect(page).toHaveURL(new RegExp(`/agents/${agentId}/configure(?:\\?.*)?$`))
  await expect(page.getByRole('heading', { name: 'Configure' })).toBeVisible({ timeout: 30_000 })
})

When(
  'I open the preseeded Agent v2 configure page for {string} from the Agent Roster',
  async function (this: DifyWorld, agentName: string) {
    const page = this.getPage()
    const agent = getPreseededAgent(this, agentName)

    await page.goto('/agents')
    await page.getByRole('link', { name: agentName }).click()
    await expect(page).toHaveURL(new RegExp(`/agents/${agent.id}/configure(?:\\?.*)?$`))
    await expect(page.getByRole('heading', { name: 'Configure' })).toBeVisible({ timeout: 30_000 })
  },
)

When(
  'I fill the Agent v2 prompt editor with the normal E2E prompt',
  async function (this: DifyWorld) {
    await fillAgentPromptEditor(this.getPage(), normalAgentPrompt)
  },
)

When(
  'I fill the Agent v2 prompt editor with the updated E2E prompt',
  async function (this: DifyWorld) {
    await fillAgentPromptEditor(this.getPage(), updatedAgentPrompt)
  },
)

When('I open the same Agent v2 configure page in another tab', async function (this: DifyWorld) {
  if (!this.context)
    throw new Error('Playwright context has not been initialized for this scenario.')

  const agentId = getCurrentAgentId(this)
  const concurrentPage = await this.context.newPage()
  this.agentBuilder.configure.concurrentPage = concurrentPage

  await concurrentPage.goto(getAgentConfigurePath(agentId))
  await expect(concurrentPage).toHaveURL(new RegExp(`/agents/${agentId}/configure(?:\\?.*)?$`))
  await expect(concurrentPage.getByRole('heading', { name: 'Configure' })).toBeVisible({
    timeout: 30_000,
  })
})

When('I save the Agent v2 prompt from the first configure tab', async function (this: DifyWorld) {
  const agentId = getCurrentAgentId(this)

  await fillAgentPromptEditor(this.getPage(), concurrentFirstAgentPrompt)
  await waitForAgentConfigureAutosaved(this.getPage())
  await expectAgentComposerPrompt(this, agentId, concurrentFirstAgentPrompt)
})

When('I save the Agent v2 prompt from the second configure tab', async function (this: DifyWorld) {
  const agentId = getCurrentAgentId(this)
  const concurrentPage = this.agentBuilder.configure.concurrentPage
  if (!concurrentPage)
    throw new Error('Open the same Agent v2 configure page in another tab before editing it.')

  await fillAgentPromptEditor(concurrentPage, concurrentSecondAgentPrompt)
  await waitForAgentConfigureAutosaved(concurrentPage)
  await expectAgentComposerPrompt(this, agentId, concurrentSecondAgentPrompt)
})

When('I refresh both Agent v2 configure tabs', async function (this: DifyWorld) {
  const page = this.getPage()
  const concurrentPage = this.agentBuilder.configure.concurrentPage
  if (!concurrentPage)
    throw new Error('Open the same Agent v2 configure page in another tab before refreshing it.')

  await Promise.all([page.reload(), concurrentPage.reload()])
  await expect(page.getByRole('heading', { name: 'Configure' })).toBeVisible({ timeout: 30_000 })
  await expect(concurrentPage.getByRole('heading', { name: 'Configure' })).toBeVisible({
    timeout: 30_000,
  })
})

Then('I should be on the Agent v2 configure page', async function (this: DifyWorld) {
  const agentId = getCurrentAgentId(this)

  await expect(this.getPage()).toHaveURL(new RegExp(`/agents/${agentId}/configure(?:\\?.*)?$`))
})

Then('I should see the Agent v2 configure workspace', async function (this: DifyWorld) {
  const page = this.getPage()
  const agentName = this.lastCreatedAgentName
  if (!agentName) throw new Error('No Agent v2 name found. Create an Agent v2 test agent first.')

  await expect(page.getByRole('region', { name: 'Configure' })).toBeVisible({ timeout: 30_000 })
  await expect(page.getByRole('heading', { name: 'Configure' })).toBeVisible()
  await expect(page.getByText(agentName, { exact: true })).toBeVisible()
})

Then(
  'I should see the normal E2E prompt in the Agent v2 prompt editor',
  async function (this: DifyWorld) {
    const page = this.getPage()

    await expect(getPromptEditor(page)).toContainText(normalAgentPrompt, { timeout: 30_000 })
  },
)

Then(
  'I should see the stable E2E model in the Agent v2 model selector',
  async function (this: DifyWorld) {
    const stableModel = this.agentBuilder.fixtures.stableModel
    if (!stableModel)
      throw new Error('Stable chat model fixture setup must run before asserting the Agent model.')

    await expect(this.getPage().getByText(stableModel.name, { exact: true })).toBeVisible({
      timeout: 30_000,
    })
  },
)

Then(
  'I should see the agent-decision E2E model in the Agent v2 model selector',
  async function (this: DifyWorld) {
    const model = this.agentBuilder.fixtures.agentDecisionModel
    if (!model)
      throw new Error(
        'Agent-decision chat model fixture setup must run before asserting the Agent model.',
      )

    await expect(this.getPage().getByText(model.name, { exact: true })).toBeVisible({
      timeout: 30_000,
    })
  },
)

Then(
  'I should see the updated E2E prompt in the Agent v2 prompt editor',
  async function (this: DifyWorld) {
    const page = this.getPage()

    await expect(getPromptEditor(page)).toContainText(updatedAgentPrompt, { timeout: 30_000 })
  },
)

Then(
  'both Agent v2 configure tabs and the Agent v2 draft should show one saved concurrent prompt',
  async function (this: DifyWorld) {
    const agentId = getCurrentAgentId(this)
    const concurrentPage = this.agentBuilder.configure.concurrentPage
    if (!concurrentPage)
      throw new Error(
        'Open the same Agent v2 configure page in another tab before asserting convergence.',
      )

    let savedPrompt = ''
    await expect
      .poll(
        async () => {
          const draft = await this.getConsoleClient().agent.byAgentId.composer.get({
            params: { agent_id: agentId },
          })
          const prompt = draft.agent_soul?.prompt?.system_prompt
          if (prompt && concurrentAgentPrompts.includes(prompt)) savedPrompt = prompt

          return !!savedPrompt
        },
        { timeout: 30_000 },
      )
      .toBe(true)

    await expect(getPromptEditor(this.getPage())).toContainText(savedPrompt)
    await expect(getPromptEditor(concurrentPage)).toContainText(savedPrompt)
  },
)

Then(
  'the normal Agent v2 draft should still use the normal E2E prompt',
  async function (this: DifyWorld) {
    await expectNormalAgentPromptDraft(this)
  },
)

Then(
  'the normal Agent v2 draft should use the normal E2E prompt',
  async function (this: DifyWorld) {
    await expectNormalAgentPromptDraft(this)
  },
)

Then(
  'the normal Agent v2 draft should use the updated E2E prompt',
  async function (this: DifyWorld) {
    await expect
      .poll(
        async () => {
          const agentId = getCurrentAgentId(this)
          const draft = await this.getConsoleClient().agent.byAgentId.composer.get({
            params: { agent_id: agentId },
          })
          return draft.agent_soul?.prompt
        },
        { timeout: 30_000 },
      )
      .toEqual({ system_prompt: updatedAgentPrompt })
  },
)

Then('the Agent v2 draft should use the stable E2E model', async function (this: DifyWorld) {
  const stableModel = this.agentBuilder.fixtures.stableModel
  if (!stableModel)
    throw new Error('Stable chat model fixture setup must run before asserting the Agent model.')

  await expect
    .poll(
      async () => {
        const agentId = getCurrentAgentId(this)
        const draft = await this.getConsoleClient().agent.byAgentId.composer.get({
          params: { agent_id: agentId },
        })
        const model = draft.agent_soul?.model
        const modelConfig =
          typeof model === 'object' && model !== null && !Array.isArray(model)
            ? (model as Record<string, unknown>)
            : undefined

        return {
          model: modelConfig?.model,
          provider: modelConfig?.model_provider,
        }
      },
      { timeout: 30_000 },
    )
    .toEqual({
      model: stableModel.name,
      provider: stableModel.provider,
    })
})

Then(
  'the Agent v2 draft should use the agent-decision E2E model',
  async function (this: DifyWorld) {
    const model = this.agentBuilder.fixtures.agentDecisionModel
    if (!model)
      throw new Error(
        'Agent-decision chat model fixture setup must run before asserting the Agent model.',
      )

    await expect
      .poll(
        async () => {
          const agentId = getCurrentAgentId(this)
          const draft = await this.getConsoleClient().agent.byAgentId.composer.get({
            params: { agent_id: agentId },
          })
          const draftModel = draft.agent_soul?.model
          const modelConfig =
            typeof draftModel === 'object' && draftModel !== null && !Array.isArray(draftModel)
              ? (draftModel as Record<string, unknown>)
              : undefined

          return {
            model: modelConfig?.model,
            provider: modelConfig?.model_provider,
          }
        },
        { timeout: 30_000 },
      )
      .toEqual({
        model: model.name,
        provider: model.provider,
      })
  },
)
