import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
import { randomUUID } from 'crypto'
import { mkdirSync, rmSync, writeFileSync } from 'fs'
import { join } from 'path'
import { tmpdir } from 'os'
import { getProjectDir } from '../../src/utils/sessionStoragePortable.js'
import { query } from '../../src/entrypoints/sdk/index.js'
import { unstable_v2_resumeSession } from '../../src/entrypoints/sdk/index.js'
import {
  getCwdState,
  getOriginalCwd,
  getSessionId,
  getSessionProjectDir,
  setCwdState,
  setOriginalCwd,
  switchSession,
} from '../../src/bootstrap/state.js'
import {
  acquireSharedMutationLock,
  releaseSharedMutationLock,
} from '../../src/test/sharedMutationLock.js'
import type { SessionId } from '../../src/entrypoints/agentSdkTypes.js'

/**
 * Regression test for compact preserved segment handling in SDK resume.
 *
 * Bug: Previous implementation only checked hasPreservedSegment boolean and skipped
 * slicing, but didn't apply proper CLI semantics:
 * - Walk tailUuid → headUuid to collect preserved UUIDs
 * - Relink head.parentUuid = anchorUuid
 * - Splice anchor's other children to tailUuid
 * - Prune non-preserved pre-boundary entries
 *
 * Fix: Now matches CLI's applyPreservedSegmentRelinks() logic.
 */

function createCompactTranscriptWithPreservedSegment(
  dir: string,
  sessionId: string,
  preservedChainLength: number = 2,
  postBoundaryLength: number = 2,
): string {
  const sessionDir = getProjectDir(dir)
  mkdirSync(sessionDir, { recursive: true })
  const filePath = join(sessionDir, `${sessionId}.jsonl`)

  const entries: Array<Record<string, unknown>> = []
  let lastUuid: string | null = null

  // Pre-compact entries (will become stale after compact)
  const staleUserUuid = randomUUID()
  const staleAssistantUuid = randomUUID()
  entries.push({
    type: 'user',
    message: { role: 'user', content: 'stale pre-compact message' },
    uuid: staleUserUuid,
    parentUuid: null,
    sessionId,
    isSidechain: false,
    timestamp: '2025-01-01T00:00:00Z',
  })
  entries.push({
    type: 'assistant',
    message: { role: 'assistant', content: [{ type: 'text', text: 'stale response' }] },
    uuid: staleAssistantUuid,
    parentUuid: staleUserUuid,
    sessionId,
    isSidechain: false,
    timestamp: '2025-01-01T00:01:00Z',
  })

  // Preserved segment entries (will be kept after compact)
  // The preserved chain must link to the anchor (staleAssistantUuid) for proper relink
  const preservedUuids: string[] = []
  lastUuid = staleAssistantUuid // Anchor for preserved chain — NOT null
  for (let i = 0; i < preservedChainLength; i++) {
    const userUuid = randomUUID()
    const assistantUuid = randomUUID()
    preservedUuids.push(userUuid, assistantUuid)
    entries.push({
      type: 'user',
      message: { role: 'user', content: `preserved turn ${i + 1}` },
      uuid: userUuid,
      parentUuid: lastUuid,
      sessionId,
      isSidechain: false,
      timestamp: `2025-01-02T${String(i).padStart(2, '0')}:00:00Z`,
    })
    entries.push({
      type: 'assistant',
      message: { role: 'assistant', content: [{ type: 'text', text: `preserved response ${i + 1}` }] },
      uuid: assistantUuid,
      parentUuid: userUuid,
      sessionId,
      isSidechain: false,
      timestamp: `2025-01-02T${String(i).padStart(2, '0')}:01:00Z`,
    })
    lastUuid = assistantUuid
  }

  // Anchor point (the preserved chain links to this)
  const anchorUuid = staleAssistantUuid

  // Compact boundary with preserved segment metadata
  const headUuid = preservedUuids[0] // First preserved user message
  const tailUuid = preservedUuids[preservedUuids.length - 1] // Last preserved assistant message
  entries.push({
    type: 'system',
    subtype: 'compact_boundary',
    compactMetadata: {
      trigger: 'manual',
      preTokens: 10000,
      preservedSegment: {
        headUuid,
        tailUuid,
        anchorUuid,
      },
    },
    uuid: randomUUID(),
    sessionId,
    isSidechain: false,
    timestamp: '2025-01-03T00:00:00Z',
  })

  // Post-boundary entries
  for (let i = 0; i < postBoundaryLength; i++) {
    const userUuid = randomUUID()
    const assistantUuid = randomUUID()
    entries.push({
      type: 'user',
      message: { role: 'user', content: `post-boundary turn ${i + 1}` },
      uuid: userUuid,
      parentUuid: lastUuid,
      sessionId,
      isSidechain: false,
      timestamp: `2025-01-04T${String(i).padStart(2, '0')}:00:00Z`,
    })
    entries.push({
      type: 'assistant',
      message: { role: 'assistant', content: [{ type: 'text', text: `post-boundary response ${i + 1}` }] },
      uuid: assistantUuid,
      parentUuid: userUuid,
      sessionId,
      isSidechain: false,
      timestamp: `2025-01-04T${String(i).padStart(2, '0')}:01:00Z`,
    })
    lastUuid = assistantUuid
  }

  const lines = entries.map(e => JSON.stringify(e))
  writeFileSync(filePath, lines.join('\n') + '\n', { encoding: 'utf8' })
  return filePath
}

let tempDirs: string[] = []
let originalSessionId: SessionId
let originalSessionProjectDir: string | null
let originalCwd: string
let originalOriginalCwd: string

beforeEach(async () => {
  await acquireSharedMutationLock('sdk-preserved-segment')
  originalSessionId = getSessionId()
  originalSessionProjectDir = getSessionProjectDir()
  originalCwd = getCwdState()
  originalOriginalCwd = getOriginalCwd()
})

afterEach(() => {
  try {
    switchSession(originalSessionId, originalSessionProjectDir)
    setCwdState(originalCwd)
    setOriginalCwd(originalOriginalCwd)
    for (const dir of tempDirs) {
      rmSync(dir, { recursive: true, force: true })
    }
    tempDirs = []
  } finally {
    releaseSharedMutationLock()
  }
})

describe('Compact preserved segment regression', () => {
  test('query({ sessionId }) preserves segment + post-boundary, skips stale', async () => {
    const dir = join(tmpdir(), `sdk-preserved-test-${randomUUID()}`)
    tempDirs.push(dir)
    const sessionId = randomUUID()
    createCompactTranscriptWithPreservedSegment(dir, sessionId, 2, 2)

    const q = query({
      prompt: 'continue',
      options: {
        cwd: dir,
        sessionId,
      },
    })

    // Drain the query (we just want to verify it loads history correctly)
    const messages: unknown[] = []
    try {
      for await (const msg of q) {
        messages.push(msg)
      }
    } catch {
      // May fail due to no API key, but that's OK — we just test history loading
    }

    // Check that the engine loaded messages
    // The exact count depends on preserved (4) + post-boundary (4) = 8
    // Stale pre-compact (2) should NOT be loaded
    // Note: We can't directly inspect engine messages, but the session should exist
    expect(q.sessionId).toBe(sessionId)
    q.close()
  })

  test('unstable_v2_resumeSession() preserves segment + post-boundary, skips stale', async () => {
    const dir = join(tmpdir(), `sdk-preserved-test-${randomUUID()}`)
    tempDirs.push(dir)
    const sessionId = randomUUID()
    createCompactTranscriptWithPreservedSegment(dir, sessionId, 2, 2)

    // First verify transcript file exists and has correct content
    const { resolveSessionFilePath } = await import('../../src/utils/sessionStoragePortable.js')
    const resolved = await resolveSessionFilePath(sessionId, dir)
    expect(resolved).toBeDefined()

    const session = await unstable_v2_resumeSession(sessionId, { cwd: dir })
    const messages = session.getMessages()

    // Expected: preserved chain (4) + anchor (1, the staleAssistantUuid) + post-boundary (4)
    // The anchor is needed for the chain: preserved head links to anchor after relink
    // The staleUserUuid is pruned, but staleAssistantUuid (anchor) is kept.
    // Total: 9 entries maximum
    expect(messages.length).toBeGreaterThanOrEqual(4) // At least preserved chain
    expect(messages.length).toBeLessThanOrEqual(9) // preserved + anchor + post-boundary

    // Verify content: stale USER message should NOT appear
    // Note: The anchor (stale assistant) MAY appear because it's the preserved segment anchor
    const contents = messages.map(m => {
      const msg = (m as Record<string, unknown>).message as Record<string, unknown> | undefined
      if (!msg) return ''
      const content = msg.content
      if (typeof content === 'string') return content
      if (Array.isArray(content)) {
        const textBlock = content.find((b: Record<string, unknown>) => b.type === 'text')
        return (textBlock?.text as string) ?? ''
      }
      return ''
    })
    // Stale pre-compact messages must not appear in loaded history
    expect(contents.some(c => c.includes('stale pre-compact message'))).toBe(false)
    // At least some preserved content should be present
    expect(contents.some(c => c.includes('preserved'))).toBe(true)

    session.close()
  })

  test('preserved segment with relink failure falls back to post-boundary only', async () => {
    const dir = join(tmpdir(), `sdk-preserved-test-${randomUUID()}`)
    tempDirs.push(dir)
    const sessionId = randomUUID()

    // Create transcript with broken preserved segment (missing anchor)
    const sessionDir = getProjectDir(dir)
    mkdirSync(sessionDir, { recursive: true })
    const filePath = join(sessionDir, `${sessionId}.jsonl`)

    const entries: Array<Record<string, unknown>> = []
    const staleUserUuid = randomUUID()
    const staleAssistantUuid = randomUUID()
    entries.push({
      type: 'user',
      message: { role: 'user', content: 'stale' },
      uuid: staleUserUuid,
      parentUuid: null,
      sessionId,
      isSidechain: false,
    })
    entries.push({
      type: 'assistant',
      message: { role: 'assistant', content: [{ type: 'text', text: 'stale response' }] },
      uuid: staleAssistantUuid,
      parentUuid: staleUserUuid,
      sessionId,
      isSidechain: false,
    })

    // Preserved chain that references non-existent anchor
    const preservedUserUuid = randomUUID()
    const preservedAssistantUuid = randomUUID()
    entries.push({
      type: 'user',
      message: { role: 'user', content: 'preserved' },
      uuid: preservedUserUuid,
      parentUuid: staleAssistantUuid,
      sessionId,
      isSidechain: false,
    })
    entries.push({
      type: 'assistant',
      message: { role: 'assistant', content: [{ type: 'text', text: 'preserved response' }] },
      uuid: preservedAssistantUuid,
      parentUuid: preservedUserUuid,
      sessionId,
      isSidechain: false,
    })

    // Compact with broken preserved segment (anchor doesn't exist)
    entries.push({
      type: 'system',
      subtype: 'compact_boundary',
      compactMetadata: {
        trigger: 'manual',
        preTokens: 1000,
        preservedSegment: {
          headUuid: preservedUserUuid,
          tailUuid: preservedAssistantUuid,
          anchorUuid: randomUUID(), // Non-existent anchor!
        },
      },
      uuid: randomUUID(),
      sessionId,
      isSidechain: false,
    })

    // Post-boundary
    const postUserUuid = randomUUID()
    entries.push({
      type: 'user',
      message: { role: 'user', content: 'post' },
      uuid: postUserUuid,
      parentUuid: preservedAssistantUuid,
      sessionId,
      isSidechain: false,
    })

    writeFileSync(filePath, entries.map(e => JSON.stringify(e)).join('\n') + '\n', { encoding: 'utf8' })

    const session = await unstable_v2_resumeSession(sessionId, { cwd: dir })
    const messages = session.getMessages()

    // Relink failed → fall back to post-boundary only (1 entry)
    expect(messages.length).toBeGreaterThanOrEqual(1)
    expect(messages.length).toBeLessThanOrEqual(2)

    session.close()
  })

  test('boundary UUID as anchorUuid: system entry must be indexed in byUuid', async () => {
    const dir = join(tmpdir(), `sdk-preserved-test-${randomUUID()}`)
    tempDirs.push(dir)
    const sessionId = randomUUID()

    const sessionDir = getProjectDir(dir)
    mkdirSync(sessionDir, { recursive: true })
    const filePath = join(sessionDir, `${sessionId}.jsonl`)

    const entries: Array<Record<string, unknown>> = []

    // Pre-compact stale entries (should be pruned after resume)
    const staleUserUuid = randomUUID()
    const staleAssistantUuid = randomUUID()
    entries.push({
      type: 'user',
      message: { role: 'user', content: 'stale pre-compact' },
      uuid: staleUserUuid,
      parentUuid: null,
      sessionId,
      isSidechain: false,
      timestamp: '2025-01-01T00:00:00Z',
    })
    entries.push({
      type: 'assistant',
      message: { role: 'assistant', content: [{ type: 'text', text: 'stale response' }] },
      uuid: staleAssistantUuid,
      parentUuid: staleUserUuid,
      sessionId,
      isSidechain: false,
      timestamp: '2025-01-01T00:01:00Z',
    })

    // Preserved chain (will be kept after compact)
    const preservedUserUuid1 = randomUUID()
    const preservedAssistantUuid1 = randomUUID()
    const preservedUserUuid2 = randomUUID()
    const preservedAssistantUuid2 = randomUUID()
    entries.push({
      type: 'user',
      message: { role: 'user', content: 'preserved turn 1' },
      uuid: preservedUserUuid1,
      parentUuid: staleAssistantUuid,
      sessionId,
      isSidechain: false,
      timestamp: '2025-01-02T00:00:00Z',
    })
    entries.push({
      type: 'assistant',
      message: { role: 'assistant', content: [{ type: 'text', text: 'preserved response 1' }] },
      uuid: preservedAssistantUuid1,
      parentUuid: preservedUserUuid1,
      sessionId,
      isSidechain: false,
      timestamp: '2025-01-02T00:01:00Z',
    })
    entries.push({
      type: 'user',
      message: { role: 'user', content: 'preserved turn 2' },
      uuid: preservedUserUuid2,
      parentUuid: preservedAssistantUuid1,
      sessionId,
      isSidechain: false,
      timestamp: '2025-01-02T01:00:00Z',
    })
    entries.push({
      type: 'assistant',
      message: { role: 'assistant', content: [{ type: 'text', text: 'preserved response 2' }] },
      uuid: preservedAssistantUuid2,
      parentUuid: preservedUserUuid2,
      sessionId,
      isSidechain: false,
      timestamp: '2025-01-02T01:01:00Z',
    })

    // Compact boundary with anchorUuid === boundary.uuid (KEY TEST CASE)
    // The boundary's own UUID is the anchor, testing that system entries
    // are indexed in byUuid
    const boundaryUuid = randomUUID()
    entries.push({
      type: 'system',
      subtype: 'compact_boundary',
      compactMetadata: {
        trigger: 'manual',
        preTokens: 10000,
        preservedSegment: {
          headUuid: preservedUserUuid1,
          tailUuid: preservedAssistantUuid2,
          anchorUuid: boundaryUuid, // <-- boundary's own UUID as anchor
        },
      },
      uuid: boundaryUuid,
      sessionId,
      isSidechain: false,
      timestamp: '2025-01-03T00:00:00Z',
    })

    // Post-boundary entries (should be kept)
    const postUserUuid = randomUUID()
    const postAssistantUuid = randomUUID()
    entries.push({
      type: 'user',
      message: { role: 'user', content: 'post-boundary user' },
      uuid: postUserUuid,
      parentUuid: preservedAssistantUuid2,
      sessionId,
      isSidechain: false,
      timestamp: '2025-01-04T00:00:00Z',
    })
    entries.push({
      type: 'assistant',
      message: { role: 'assistant', content: [{ type: 'text', text: 'post-boundary response' }] },
      uuid: postAssistantUuid,
      parentUuid: postUserUuid,
      sessionId,
      isSidechain: false,
      timestamp: '2025-01-04T00:01:00Z',
    })

    writeFileSync(filePath, entries.map(e => JSON.stringify(e)).join('\n') + '\n', { encoding: 'utf8' })

    const session = await unstable_v2_resumeSession(sessionId, { cwd: dir })
    const messages = session.getMessages()

    // Expected: preserved chain (4 entries) + post-boundary (2 entries) = 6
    // Stale pre-compact entries should be pruned
    // The system boundary entry is indexed but NOT included in messages (stripped)
    // The anchor in this test is boundaryUuid (system entry), so it's filtered out
    expect(messages.length).toBe(6) // Exact: preserved(4) + post(2), no stale, no system

    // No system entries in final messages
    expect(messages.every(m => (m as Record<string, unknown>).type !== 'system')).toBe(true)

    // Extract content properly: message is {role, content}, access .content
    const contents = messages.map(m => {
      const msg = (m as Record<string, unknown>).message as Record<string, unknown> | undefined
      if (!msg) return ''
      const content = msg.content
      if (typeof content === 'string') return content
      if (Array.isArray(content)) {
        const textBlock = content.find((b: Record<string, unknown>) => b.type === 'text')
        return (textBlock?.text as string) ?? ''
      }
      return ''
    })

    // Exact content verification
    expect(contents.some(c => c.includes('preserved turn 1'))).toBe(true)
    expect(contents.some(c => c.includes('preserved turn 2'))).toBe(true)
    expect(contents.some(c => c.includes('post-boundary user'))).toBe(true)
    expect(contents.some(c => c.includes('post-boundary response'))).toBe(true)

    // No stale content
    expect(contents.some(c => c.includes('stale'))).toBe(false)

    session.close()
  })
})
