/**
 * U3 — Calendar data consistency E2E.
 *
 * Regression coverage for issue #2 (2026-06-28): calendar events created
 * via the API (or agent tool) were not visible in the UI calendar panel.
 * Root cause was a user_id mismatch — the CalendarTool's default_user_id
 * resolved to a stale admin row while the UI queried events for the
 * logged-in JWT user_id.
 *
 * Strategy:
 * - Create events via the REST API (POST /calendar/events) as the test
 *   user (JWT-scoped) — this mirrors the "user creates event" path.
 * - Verify the events appear in the UI calendar panel.
 * - Verify user_id integrity: the event's user_id must match the logged-
 *   in user, not a hallucinated "default" / "zhangsan" value.
 * - Verify delete syncs between API and UI.
 *
 * ponytail: the agent-created path (ReAct → CalendarTool) uses
 * default_user_id which is resolved once at server startup from the
 * first admin row in auth.db. In dev/test mode with a stale admin user,
 * this may NOT match the logged-in JWT user_id — a known single-user
 * simplification (see app.py:440 comment). We do NOT test the agent
 * path here; that's covered by the calendar.spec.ts agent-flow tests.
 */

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

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

const API_BASE = 'http://127.0.0.1:8000/api/v1'
const CALENDAR_BASE = `${API_BASE}/calendar`
const AUTH_BASE = `${API_BASE}/auth`

let _cachedToken: string | null = null
let _cachedUserId: string | null = null

async function getAccessToken(): Promise<string> {
  if (_cachedToken) return _cachedToken
  const resp = await fetch(`${AUTH_BASE}/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
}

/** Get the test user's user_id by decoding the JWT's `sub` claim. */
async function getUserId(): Promise<string> {
  if (_cachedUserId) return _cachedUserId
  const token = await getAccessToken()
  // JWT format: header.payload.signature — decode payload (base64url).
  const payload = token.split('.')[1]
  const decoded = JSON.parse(
    Buffer.from(payload, 'base64url').toString('utf-8'),
  ) as { sub: string }
  _cachedUserId = decoded.sub
  return _cachedUserId
}

interface CalendarEvent {
  id: string
  title: string
  user_id: string
  start_time: string
  end_time: string
}

/** Create an event via the REST API as the test user. */
async function createEventViaApi(opts: {
  title: string
  startIso: string
  endIso: string
  description?: string
}): Promise<CalendarEvent> {
  const token = await getAccessToken()
  const resp = await fetch(`${CALENDAR_BASE}/events`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      title: opts.title,
      start_time: opts.startIso,
      end_time: opts.endIso,
      description: opts.description ?? '',
    }),
  })
  if (!resp.ok) {
    throw new Error(`createEventViaApi failed: ${resp.status} ${await resp.text()}`)
  }
  const body = (await resp.json()) as { event: CalendarEvent }
  return body.event
}

/** List events via the REST API as the test user. */
async function listEventsViaApi(): Promise<CalendarEvent[]> {
  const token = await getAccessToken()
  const resp = await fetch(`${CALENDAR_BASE}/events`, {
    headers: { Authorization: `Bearer ${token}` },
  })
  if (!resp.ok) throw new Error(`listEventsViaApi failed: ${resp.status}`)
  const body = (await resp.json()) as { events: CalendarEvent[] }
  return body.events
}

/** Delete an event via the REST API. */
async function deleteEventViaApi(id: string): Promise<void> {
  const token = await getAccessToken()
  const resp = await fetch(`${CALENDAR_BASE}/events/${id}`, {
    method: 'DELETE',
    headers: { Authorization: `Bearer ${token}` },
  })
  if (!resp.ok) throw new Error(`deleteEventViaApi failed: ${resp.status}`)
}

/** Delete all events whose title starts with the e2e prefix. */
async function cleanupE2EEvents(): Promise<void> {
  const events = await listEventsViaApi()
  await Promise.all(
    events
      .filter((e) => e.title.startsWith('E2E_'))
      .map((e) => deleteEventViaApi(e.id).catch(() => {})),
  )
}

// ── Time helpers ───────────────────────────────────────────────────────

/**
 * Return an ISO 8601 timestamp `hoursFromNow` hours in the future.
 * The backend stores events in UTC and the calendar panel renders
 * "today" / "upcoming" relative to the server's local time. Using
 * future timestamps ensures the events show up in "今日" or "即将到来".
 */
function isoHoursFromNow(hoursFromNow: number): string {
  const d = new Date(Date.now() + hoursFromNow * 3600_000)
  // toISOString() returns UTC with 'Z' suffix — the backend accepts this.
  return d.toISOString()
}

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

/**
 * Login and navigate to a workbench view (not /agent/chat which uses the
 * chat-only layout). Then click the "日历" tab in the bottom-right
 * quadrant to mount the CalendarPanel.
 *
 * Follows the proven pattern from calendar.spec.ts: SPA-navigate via the
 * TopNav "setting" button (avoids cold-start whoami timing issues that
 * page.goto('/agent/monitor') can trigger).
 */
async function loginAndOpenCalendar(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()

  // Wait for redirect to /agent
  await expect(page).toHaveURL(/\/agent/, { timeout: 15_000 })

  // Click the settings icon in TopNav to SPA-navigate to /agent/monitor
  // (workbench layout). The button's accessible name is "setting".
  await page.getByRole('button', { name: 'setting' }).click()

  // Wait for the QuadrantPanel tabs to render (workbench layout).
  await expect(page.locator('.quadrant-panel__tab').first()).toBeVisible({
    timeout: 15_000,
  })

  // Click the "日历" tab in the bottom-right panel.
  const calendarTab = page.locator('.quadrant-panel__tab', { hasText: '日历' })
  await calendarTab.click()

  // The CalendarPanel should mount and show its header.
  await expect(page.locator('.calendar-panel')).toBeVisible({ timeout: 10_000 })
  // Wait for loading to finish (the "加载日程..." spinner disappears).
  await expect(page.locator('.calendar-panel__loading')).toBeHidden({
    timeout: 15_000,
  })
}

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

test.describe('Calendar data consistency', () => {
  test.beforeEach(async () => {
    await cleanupE2EEvents()
  })

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

  test('event created via API is visible in the UI calendar panel', async ({ page }) => {
    const title = `E2E_API_VISIBLE_${Date.now().toString(36)}`
    await createEventViaApi({
      title,
      startIso: isoHoursFromNow(2),
      endIso: isoHoursFromNow(3),
    })

    await loginAndOpenCalendar(page)

    // The event should appear in the calendar panel — either in "今日"
    // or "即将到来" depending on the exact time. Poll because the panel
    // fetches events on mount.
    await expect
      .poll(
        async () => {
          const items = page.locator('.calendar-panel__item')
          const count = await items.count()
          for (let i = 0; i < count; i++) {
            const text = await items.nth(i).textContent()
            if (text && text.includes(title)) return true
          }
          return false
        },
        { timeout: 15_000, intervals: [500] },
      )
      .toBe(true)
  })

  test('event user_id matches the logged-in user, not a hallucinated value', async () => {
    const title = `E2E_UID_${Date.now().toString(36)}`
    const created = await createEventViaApi({
      title,
      startIso: isoHoursFromNow(4),
      endIso: isoHoursFromNow(5),
    })

    const expectedUserId = await getUserId()

    // Direct field check on the creation response.
    expect(created.user_id).toBe(expectedUserId)

    // Re-query via list to verify persistence.
    const events = await listEventsViaApi()
    const found = events.find((e) => e.id === created.id)
    expect(found).toBeDefined()
    expect(found?.user_id).toBe(expectedUserId)

    // Guard against the regression: user_id must never be a hallucinated
    // placeholder value. These are the values the agent tool used to
    // produce when default_user_id resolution failed.
    const hallucinated = ['default', 'zhangsan', 'admin', 'test', '']
    expect(hallucinated).not.toContain(found?.user_id)
  })

  test('multiple events all render in the calendar panel', async ({ page }) => {
    const titles = [
      `E2E_MULTI_1_${Date.now().toString(36)}`,
      `E2E_MULTI_2_${Date.now().toString(36)}`,
      `E2E_MULTI_3_${Date.now().toString(36)}`,
    ]
    // Stagger: +6h, +12h, +24h so they land in "今日" / "即将到来".
    for (let i = 0; i < titles.length; i++) {
      await createEventViaApi({
        title: titles[i],
        startIso: isoHoursFromNow(6 + i * 6),
        endIso: isoHoursFromNow(7 + i * 6),
      })
    }

    await loginAndOpenCalendar(page)

    // Each title should appear in the panel.
    for (const title of titles) {
      await expect
        .poll(
          async () => {
            const items = page.locator('.calendar-panel__item')
            const count = await items.count()
            for (let i = 0; i < count; i++) {
              const text = await items.nth(i).textContent()
              if (text && text.includes(title)) return true
            }
            return false
          },
          { timeout: 15_000, intervals: [500] },
        )
        .toBe(true)
    }
  })

  test('deleting an event via API syncs to the UI after reload', async ({ page }) => {
    const title = `E2E_DELETE_SYNC_${Date.now().toString(36)}`
    const event = await createEventViaApi({
      title,
      startIso: isoHoursFromNow(8),
      endIso: isoHoursFromNow(9),
    })

    await loginAndOpenCalendar(page)

    // Verify the event is visible first.
    await expect
      .poll(
        async () => {
          const items = page.locator('.calendar-panel__item')
          const count = await items.count()
          for (let i = 0; i < count; i++) {
            const text = await items.nth(i).textContent()
            if (text && text.includes(title)) return true
          }
          return false
        },
        { timeout: 15_000, intervals: [500] },
      )
      .toBe(true)

    // Delete via API.
    await deleteEventViaApi(event.id)

    // Verify API no longer returns it.
    const eventsAfterDelete = await listEventsViaApi()
    expect(eventsAfterDelete.find((e) => e.id === event.id)).toBeUndefined()

    // Reload the page — the UI must reflect the deletion. After reload
    // the auth state is preserved (U1 covers this), and the page returns
    // to /agent/monitor where the workbench layout remounts.
    await page.reload()
    await expect(page).toHaveURL(/\/agent\/monitor/, { timeout: 15_000 })
    // Wait for the QuadrantPanel tabs to render, then click 日历.
    await expect(page.locator('.quadrant-panel__tab').first()).toBeVisible({
      timeout: 15_000,
    })
    await page.locator('.quadrant-panel__tab', { hasText: '日历' }).click()
    await expect(page.locator('.calendar-panel')).toBeVisible({ timeout: 10_000 })
    await expect(page.locator('.calendar-panel__loading')).toBeHidden({
      timeout: 15_000,
    })

    // The deleted event must NOT reappear.
    await expect
      .poll(
        async () => {
          const items = page.locator('.calendar-panel__item')
          const count = await items.count()
          for (let i = 0; i < count; i++) {
            const text = await items.nth(i).textContent()
            if (text && text.includes(title)) return true
          }
          return false
        },
        { timeout: 10_000, intervals: [500] },
      )
      .toBe(false)
  })
})
