/**
 * U2 — Conversation management CRUD E2E.
 *
 * Regression coverage for issue #1 (2026-06-28): test conversations
 * accumulated in the sidebar and could not be deleted. Root cause was
 * pre-existing test data in SQLite + the delete button's popconfirm
 * occasionally not triggering the API call.
 *
 * Strategy:
 * - Create conversations via the REST API (POST /portal/chat) so they
 *   exist on the server before the UI loads — this mirrors the
 *   "stale conversations from a previous session" scenario.
 * - Use the UI to delete them and verify both the UI list and the
 *   server-side list reflect the deletion (no "复活").
 *
 * ponytail: portal endpoints use API-key auth, but in dev/test mode no
 * key is configured so requests pass through unauthenticated. The test
 * relies on this. If API-key enforcement is enabled in E2E, set
 * AGENTKIT_API_KEY in the playwright env and add the header here.
 */

import { test, expect, type Page } from '@playwright/test'
import { TEST_USER, clearAuth, reloadAndWaitAuth } from './helpers'

// ── API helpers ────────────────────────────────────────────────────────

const PORTAL_BASE = 'http://127.0.0.1:8000/api/v1/portal'

/** Cached access token for API calls (login once per worker). */
let _cachedToken: string | null = null

async function getAccessToken(): Promise<string> {
  if (_cachedToken) return _cachedToken
  const resp = await fetch(`${PORTAL_BASE.replace('/portal', '')}/auth/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      username: TEST_USER.username,
      password: TEST_USER.password,
    }),
  })
  if (!resp.ok) throw new Error(`Login failed: ${resp.status}`)
  const data = (await resp.json()) as { access_token: string }
  _cachedToken = data.access_token
  return _cachedToken
}

/**
 * Create a server-side conversation by sending a chat message.
 * The portal's /chat endpoint creates the conversation on first message.
 * Returns the conversation id.
 *
 * We send a trivial message and wait for the synchronous response so the
 * conversation is persisted before the test proceeds.
 */
async function createConversationViaApi(title: string): Promise<string> {
  const token = await getAccessToken()
  const resp = await fetch(`${PORTAL_BASE}/chat`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      message: title,
      conversation_id: `e2e-conv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
    }),
  })
  if (!resp.ok) {
    throw new Error(`createConversationViaApi failed: ${resp.status} ${await resp.text()}`)
  }
  const body = (await resp.json()) as { conversation_id: string }
  return body.conversation_id
}

/** List server-side conversations — used to verify deletion persisted. */
async function listConversationsViaApi(): Promise<{ id: string; title: string }[]> {
  const token = await getAccessToken()
  const resp = await fetch(`${PORTAL_BASE}/conversations`, {
    headers: { Authorization: `Bearer ${token}` },
  })
  if (!resp.ok) throw new Error(`listConversationsViaApi failed: ${resp.status}`)
  const body = (await resp.json()) as { id: string; title: string }[]
  return body
}

/** Delete all conversations whose id starts with the e2e prefix. */
async function cleanupE2EConversations(): Promise<void> {
  const convs = await listConversationsViaApi()
  for (const c of convs) {
    if (c.id.startsWith('e2e-conv-')) {
      const token = await getAccessToken()
      await fetch(`${PORTAL_BASE}/conversations/${c.id}`, {
        method: 'DELETE',
        headers: { Authorization: `Bearer ${token}` },
      })
    }
  }
}

// ── UI helpers ─────────────────────────────────────────────────────────

/** Login via UI form and wait for the chat view to mount. */
async function loginAndOpenChat(page: Page): Promise<void> {
  await page.goto('/login')
  await clearAuth(page)
  await page.getByPlaceholder('请输入用户名').fill(TEST_USER.username)
  await page.getByPlaceholder('请输入密码').fill(TEST_USER.password)
  await page.getByRole('button', { name: /登\s*录/ }).click()
  await expect(page).toHaveURL(/\/agent/, { timeout: 15_000 })
  // Wait for the chat textarea to confirm the view mounted.
  await expect(page.getByPlaceholder('输入消息，按 Enter 发送...')).toBeVisible({
    timeout: 15_000,
  })
  // Make the per-item delete button always visible — by default it's
  // display:none until :hover, and Playwright's hover-then-click is
  // flaky because the hover state can be lost between actions. Forcing
  // the button visible lets us click it directly without fighting CSS.
  await page.addStyleTag({
    content: '.chat-sidebar__item-delete { display: flex !important; }',
  })
}

/** Wait for a conversation with the given title fragment to appear in the sidebar. */
async function waitForConversationInSidebar(
  page: Page,
  titleFragment: string,
  timeoutMs = 15_000,
): Promise<void> {
  await expect
    .poll(
      async () => {
        const items = page.locator('.chat-sidebar__item')
        const count = await items.count()
        for (let i = 0; i < count; i++) {
          const text = await items.nth(i).textContent()
          if (text && text.includes(titleFragment)) return true
        }
        return false
      },
      { timeout: timeoutMs, intervals: [500] },
    )
    .toBe(true)
}

// ── Tests ──────────────────────────────────────────────────────────────

test.describe('Conversation management CRUD', () => {
  test.beforeEach(async () => {
    await cleanupE2EConversations()
  })

  test.afterEach(async () => {
    await cleanupE2EConversations()
  })

  test('create new conversation via UI, then delete it', async ({ page }) => {
    await loginAndOpenChat(page)

    // Record the current count so we can verify +1 / -1 rather than
    // asserting an absolute count (the sidebar may contain pre-existing
    // conversations from prior test sessions).
    const initialCount = await page.locator('.chat-sidebar__item').count()

    // Click "新建对话" — creates a local-only conversation in the store.
    await page.getByRole('button', { name: /新建对话/ }).click()

    // A new "新对话" item should appear at the top of the sidebar.
    await expect
      .poll(async () => {
        const items = page.locator('.chat-sidebar__item')
        return (await items.count()) - initialCount
      }, { timeout: 5_000 })
      .toBe(1)
    await expect(page.locator('.chat-sidebar__item-title').first()).toContainText('新对话')

    // The delete button is always visible (loginAndOpenChat injected CSS
    // to override the display:none-until-hover rule). Click it to open
    // the a-popconfirm.
    const deleteBtn = page.locator('.chat-sidebar__item-delete').first()
    await deleteBtn.click()

    // The popconfirm should appear. ant-design-vue renders it in a portal
    // at body level. Wait for the confirm text to be visible.
    await expect(page.getByText('确定删除此对话？')).toBeVisible({ timeout: 5_000 })

    // Click the OK button directly via DOM. Playwright's locator-based click
    // on portal-rendered buttons is flaky in ant-design-vue (the button text
    // is split into spans with inserted spaces for CJK: "删 除"), and
    // keyboard Enter doesn't reliably focus the OK button. A direct
    // .click() on the primary button element is the most reliable path.
    await page.evaluate(() => {
      const btn = document.querySelector(
        '.ant-popconfirm-buttons .ant-btn-primary',
      ) as HTMLButtonElement | null
      if (!btn) throw new Error('popconfirm ok button not found')
      btn.click()
    })

    // The newly-created "新对话" should be gone — count returns to initial.
    await expect
      .poll(async () => {
        const items = page.locator('.chat-sidebar__item')
        const titles = await page.locator('.chat-sidebar__item-title').allTextContents()
        const hasNewConv = titles.some((t) => t === '新对话')
        return { count: (await items.count()) - initialCount, hasNewConv }
      }, { timeout: 10_000 })
      .toEqual({ count: 0, hasNewConv: false })
  })

  test('server-side conversation is deleted and does not resurrect after reload', async ({
    page,
  }) => {
    // This is the core regression test for issue #1: a conversation that
    // exists on the server must be deleted from both UI and server when
    // the user clicks delete, and must not reappear after reload.
    // Marker must be ≤20 chars — backend _derive_conversation_title_from_content
    // truncates to content[:20] + "..." so a longer marker won't match in the sidebar.
    const marker = `E2E_DEL_${Date.now().toString(36)}`
    const convId = await createConversationViaApi(marker)

    await loginAndOpenChat(page)

    // The server-side conversation should appear in the sidebar.
    await waitForConversationInSidebar(page, marker)

    // Click delete + confirm (button is always visible via injected CSS).
    const item = page.locator('.chat-sidebar__item', { hasText: marker }).first()
    await item.locator('.chat-sidebar__item-delete').click()

    await expect(page.getByText('确定删除此对话？')).toBeVisible({ timeout: 5_000 })
    await page.evaluate(() => {
      const btn = document.querySelector(
        '.ant-popconfirm-buttons .ant-btn-primary',
      ) as HTMLButtonElement | null
      if (!btn) throw new Error('popconfirm ok button not found')
      btn.click()
    })

    // UI: the conversation should disappear from the sidebar.
    await expect
      .poll(
        async () => {
          const items = page.locator('.chat-sidebar__item', { hasText: marker })
          return await items.count()
        },
        { timeout: 10_000, intervals: [500] },
      )
      .toBe(0)

    // Server: the conversation must also be gone — verify via API.
    const convsAfterDelete = await listConversationsViaApi()
    expect(convsAfterDelete.find((c) => c.id === convId)).toBeUndefined()

    // Reload — the conversation must NOT resurrect (this is the bug
    // users hit when cache/state desync let deleted convs reappear).
    await reloadAndWaitAuth(page)
    await expect(page).toHaveURL(/\/agent/, { timeout: 20_000 })
    await expect(page.getByPlaceholder('输入消息，按 Enter 发送...')).toBeVisible({
      timeout: 15_000,
    })

    // Give the sidebar a moment to load the fresh conversation list.
    await expect
      .poll(
        async () => {
          const items = page.locator('.chat-sidebar__item', { hasText: marker })
          return await items.count()
        },
        { timeout: 10_000, intervals: [500] },
      )
      .toBe(0)

    // Final API sanity check.
    const convsAfterReload = await listConversationsViaApi()
    expect(convsAfterReload.find((c) => c.id === convId)).toBeUndefined()
  })

  test('switching between conversations loads the correct history', async ({ page }) => {
    // Create two server-side conversations with distinct first messages.
    // Markers must be ≤20 chars (backend truncates titles to content[:20]).
    const ts = Date.now().toString(36)
    const marker1 = `E2E_A_${ts}`
    const marker2 = `E2E_B_${ts}`
    await createConversationViaApi(marker1)
    await createConversationViaApi(marker2)

    await loginAndOpenChat(page)

    // Both should appear in the sidebar.
    await waitForConversationInSidebar(page, marker1)
    await waitForConversationInSidebar(page, marker2)

    // Click conversation 1 — its first user message should be visible.
    await page.locator('.chat-sidebar__item', { hasText: marker1 }).first().click()
    await expect
      .poll(
        async () => {
          const userBubbles = page.locator('.message-shell--user .user-bubble')
          const count = await userBubbles.count()
          if (count === 0) return false
          const text = await userBubbles.first().textContent()
          return text?.includes(marker1) ?? false
        },
        { timeout: 10_000, intervals: [500] },
      )
      .toBe(true)

    // Click conversation 2 — its first user message should now be visible
    // (history must be isolated between conversations).
    await page.locator('.chat-sidebar__item', { hasText: marker2 }).first().click()
    await expect
      .poll(
        async () => {
          const userBubbles = page.locator('.message-shell--user .user-bubble')
          const count = await userBubbles.count()
          if (count === 0) return false
          const text = await userBubbles.first().textContent()
          return text?.includes(marker2) ?? false
        },
        { timeout: 10_000, intervals: [500] },
      )
      .toBe(true)

    // The first conversation's marker should no longer be in the visible history.
    const visibleText = await page.locator('.message-shell--user .user-bubble').allTextContents()
    expect(visibleText.some((t) => t.includes(marker1))).toBe(false)
  })

  test('deleting the active conversation auto-switches to another', async ({ page }) => {
    // Markers must be ≤20 chars (backend truncates titles to content[:20]).
    const ts = Date.now().toString(36)
    const marker1 = `E2E_1_${ts}`
    const marker2 = `E2E_2_${ts}`
    await createConversationViaApi(marker1)
    await createConversationViaApi(marker2)

    await loginAndOpenChat(page)
    await waitForConversationInSidebar(page, marker1)
    await waitForConversationInSidebar(page, marker2)

    // Select the first conversation to make it active.
    await page.locator('.chat-sidebar__item', { hasText: marker1 }).first().click()
    await expect(page.locator('.chat-sidebar__item--active', { hasText: marker1 })).toBeVisible({
      timeout: 5_000,
    })

    // Delete the active conversation (button always visible via injected CSS).
    const activeItem = page.locator('.chat-sidebar__item--active')
    await activeItem.locator('.chat-sidebar__item-delete').click()
    await expect(page.getByText('确定删除此对话？')).toBeVisible({ timeout: 5_000 })
    await page.evaluate(() => {
      const btn = document.querySelector(
        '.ant-popconfirm-buttons .ant-btn-primary',
      ) as HTMLButtonElement | null
      if (!btn) throw new Error('popconfirm ok button not found')
      btn.click()
    })

    // The active conversation should be gone, and another should be active.
    await expect
      .poll(
        async () => {
          const active = page.locator('.chat-sidebar__item--active')
          const activeCount = await active.count()
          const deletedStillThere = await page
            .locator('.chat-sidebar__item', { hasText: marker1 })
            .count()
          return { activeCount, deletedStillThere }
        },
        { timeout: 10_000, intervals: [500] },
      )
      .toEqual({ activeCount: 1, deletedStillThere: 0 })
  })
})
