/** Policy inheritance through child session events appended before publication. */

import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { mkdtemp, readFile, realpath, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { Context } from 'cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import SandboxedFileSystem from '@deepseek-ai/dsh-fs-sandbox'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import SandboxPolicyService, { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
import ApprovalService, { setApprovalPolicy } from '@deepseek-ai/dsh-user-approval'
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import { startInProcessRun } from '../src/index.ts'

type Script = ConstructorParameters<typeof MockAdapter>[0]

const READ_ONLY_DENIAL = '[sandbox: file access denied under read-only mode]'
const contexts: Context[] = []
let workspace: string

beforeEach(async () => {
  workspace = await realpath(await mkdtemp(join(tmpdir(), 'dsh-inherit-')))
})

afterEach(async () => {
  for (const ctx of contexts.splice(0).reverse()) await ctx.fiber.dispose()
  await rm(workspace, { recursive: true, force: true })
})

async function setupWalled(script: Script): Promise<{ ctx: Context; parent: Agent }> {
  const ctx = new Context()
  contexts.push(ctx)
  await mountAgentLoopTestDependencies(ctx)
  await ctx.plugin(SandboxPolicyService, { mode: 'workspace-write', workspaceRoot: workspace })
  await ctx.plugin(SandboxedFileSystem, { cwd: workspace })
  await ctx.plugin(ToolFs)
  await ctx.plugin(ApprovalService)
  await ctx.plugin(AgentLoop, { agents: [] })
  ctx.llm.registerAdapter(['mock'], new MockAdapter(script))
  const parent = ctx.agentLoop.create(
    SessionId('parent'),
    { provider: 'mock', model: 'mock' },
    { cwd: workspace },
  )
  return { ctx, parent }
}

function spawnRequest(parent: Agent) {
  return {
    prompt: [{ type: 'text' as const, text: 'child task' }],
    parent,
    signal: new AbortController().signal,
  }
}

function toolResultTexts(agent: Agent): string[] {
  return agent.session.events
    .filter((event): event is SessionEvent<'tool/result'> => event.type === 'tool/result')
    .map(event => event.data.message.content
      .flatMap(block => block.content)
      .filter((block): block is Extract<ContentBlock, { type: 'text' }> => block.type === 'text')
      .map(block => block.text)
      .join(''))
}

describe('in-process policy inheritance', () => {
  it('records parent overrides before publishing a spawn child', async () => {
    const script: Script = []
    const { ctx, parent } = await setupWalled(script)
    const blocked = join(workspace, 'spawn-blocked.txt')
    setSandboxMode(parent.session, 'read-only')
    setApprovalPolicy(parent.session, 'never')
    const parentLogLength = parent.session.events.length
    script.push(
      toolCallResponse('write', 'write', { file_path: blocked, content: 'escaped' }),
      textResponse('child done'),
    )

    const run = await startInProcessRun(spawnRequest(parent), {})
    try {
      const result = await run.result
      const child = run.localAgent as Agent

      await expect(readFile(blocked, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' })
      expect(toolResultTexts(child).join('\n')).toContain(READ_ONLY_DENIAL)
      expect(result.stopReason).toBe('completed')
      expect(child.session.events.slice(0, 2)).toMatchObject([
        { type: 'sandbox/mode', seq: 0, data: { mode: 'read-only', source: 'delegation' } },
        { type: 'approval/policy', seq: 1, data: { policy: 'never', source: 'delegation' } },
      ])
      expect(child.session.firstLiveSeq).toBe(0)
      expect(child.session.header.seedLength).toBeUndefined()
      expect(ctx.sandboxPolicy.overrideOf(child.session)).toBe('read-only')
      expect(ctx.approval.overrideOf(child.session)).toBe('never')
      const request = child.session.events.find(
        (event): event is SessionEvent<'request/header'> => event.type === 'request/header',
      )
      expect(request?.data.header.system).toContain('Approval prompts are disabled')
      expect(parent.session.events).toHaveLength(parentLogLength)
    } finally {
      await run.dispose()
    }
  })

  it('places inherited events after a fork prefix so fresh policy wins stale seed state', async () => {
    const script: Script = []
    const { ctx, parent } = await setupWalled(script)
    const blocked = join(workspace, 'fork-blocked.txt')
    setSandboxMode(parent.session, 'workspace-write')
    const seed = [...parent.session.events]
    setSandboxMode(parent.session, 'read-only')
    script.push(
      toolCallResponse('write', 'write', { file_path: blocked, content: 'escaped' }),
      textResponse('child done'),
    )

    const run = await startInProcessRun(spawnRequest(parent), { seed })
    try {
      await run.result
      const child = run.localAgent as Agent

      expect(child.session.header.seedLength).toBe(1)
      expect(child.session.firstLiveSeq).toBe(seed.length)
      expect(child.session.events.filter(event => event.type === 'sandbox/mode')).toMatchObject([
        { seq: 0, data: { mode: 'workspace-write' } },
        { seq: 1, data: { mode: 'read-only', source: 'delegation' } },
      ])
      await expect(readFile(blocked, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' })
      expect(ctx.sandboxPolicy.overrideOf(child.session)).toBe('read-only')

      setSandboxMode(child.session, 'danger-full-access')
      expect(ctx.sandboxPolicy.overrideOf(child.session)).toBe('danger-full-access')
    } finally {
      await run.dispose()
    }
  })

  it('captures policy at delegation before asynchronous child creation', async () => {
    const script: Script = [textResponse('child done')]
    const { ctx, parent } = await setupWalled(script)
    setSandboxMode(parent.session, 'read-only')

    const starting = startInProcessRun(spawnRequest(parent), {})
    setSandboxMode(parent.session, 'danger-full-access')
    const run = await starting
    try {
      await run.result
      const child = run.localAgent as Agent
      expect(ctx.sandboxPolicy.overrideOf(parent.session)).toBe('danger-full-access')
      expect(ctx.sandboxPolicy.overrideOf(child.session)).toBe('read-only')
    } finally {
      await run.dispose()
    }
  })

  it('does not freeze deployment defaults into an unswitched child', async () => {
    const script: Script = []
    const { parent } = await setupWalled(script)
    const allowed = join(workspace, 'default-allowed.txt')
    script.push(
      toolCallResponse('write', 'write', { file_path: allowed, content: 'fine' }),
      textResponse('child done'),
    )

    const run = await startInProcessRun(spawnRequest(parent), {})
    try {
      await run.result
      const child = run.localAgent as Agent
      expect(await readFile(allowed, 'utf8')).toBe('fine')
      expect(child.session.events.some(
        event => event.type === 'sandbox/mode' || event.type === 'approval/policy',
      )).toBe(false)
      expect(child.session.firstLiveSeq).toBe(0)
    } finally {
      await run.dispose()
    }
  })
})
