import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import * as tool from '../src/index.ts'
import { call, dummyTool, LISTENER_CODE, REVERSE_TOOL_CODE, setup, text } from './helpers.ts'

/**
 * Disposal semantics: `cordis_unmount` reaches quiescence before returning,
 * and disposing the tool-cordis fiber itself (the HMR path) cascades over the
 * whole dynamic subtree through the ordinary parent→child fiber lifecycle.
 */

afterEach(() => {
  vi.restoreAllMocks()
})

describe('cordis_unmount', () => {
  it('disposes the mount and its registrations have stopped by the time it returns (quiescence)', async () => {
    const ctx = await setup()
    const log = vi.spyOn(console, 'log').mockImplementation(() => {})
    await call(ctx, 'cordis_mount', { code: LISTENER_CODE })

    ctx.tools.register(dummyTool('trigger_before'))
    expect(log).toHaveBeenCalledTimes(1)

    const result = await call(ctx, 'cordis_unmount', { id: 'dyn-1' })
    expect(result.isError).toBe(false)
    expect(text(result)).toContain('unmounted dyn-1')

    // Immediately after the awaited unmount, the listener must be gone — no
    // grace period, no eventual consistency.
    ctx.tools.register(dummyTool('trigger_after'))
    expect(log).toHaveBeenCalledTimes(1)
    expect(text(await call(ctx, 'cordis_inspect', { what: 'dynamic' }))).toContain('(no dynamic plugins mounted)')
  })

  it('unregisters a self-made tool on unmount', async () => {
    const ctx = await setup()
    await call(ctx, 'cordis_mount', { code: REVERSE_TOOL_CODE })
    expect(ctx.tools.get('reverse_text')).toBeDefined()

    await call(ctx, 'cordis_unmount', { id: 'dyn-1' })
    expect(ctx.tools.get('reverse_text')).toBeUndefined()
  })

  it('rejects an unknown id, and a second unmount of the same id', async () => {
    const ctx = await setup()
    const unknown = await call(ctx, 'cordis_unmount', { id: 'dyn-99' })
    expect(unknown.isError).toBe(true)
    expect(text(unknown)).toContain('no dynamic plugin with id "dyn-99"')

    await call(ctx, 'cordis_mount', { code: LISTENER_CODE })
    await call(ctx, 'cordis_unmount', { id: 'dyn-1' })
    const again = await call(ctx, 'cordis_unmount', { id: 'dyn-1' })
    expect(again.isError).toBe(true)
  })
})

describe('HMR safety', () => {
  it('disposing the tool-cordis fiber cascades over the dynamic subtree and its registrations', async () => {
    const ctx = new Context()
    await ctx.plugin(SystemPrompt)
    await ctx.plugin(ToolRegistry)
    const fiber = await ctx.plugin(tool)

    const log = vi.spyOn(console, 'log').mockImplementation(() => {})
    await call(ctx, 'cordis_mount', { code: LISTENER_CODE })
    await call(ctx, 'cordis_mount', { code: REVERSE_TOOL_CODE })
    expect(ctx.tools.get('reverse_text')).toBeDefined()

    await fiber.dispose()

    // The whole subtree is gone: the self-made tool, the cordis tools, and the
    // mounted listener (no log on a fresh tools/change).
    expect(ctx.tools.get('reverse_text')).toBeUndefined()
    expect(ctx.tools.get('cordis_mount')).toBeUndefined()
    const calls = log.mock.calls.length
    ctx.tools.register(dummyTool('trigger_post_dispose'))
    expect(log).toHaveBeenCalledTimes(calls)
  })
})
