import { describe, expect, it } from 'vitest'
import { HumanInTheLoop } from '../hitl.js'
import { Agent } from '../../../agent/agent.js'
import { MockMessageModel } from '../../../__fixtures__/mock-message-model.js'
import { createMockTool } from '../../../__fixtures__/tool-helpers.js'

describe('HumanInTheLoop', () => {
  describe('default config (interrupt/resume)', () => {
    it('pauses agent with interrupt on any tool call', async () => {
      const model = new MockMessageModel()
        .addTurn({ type: 'toolUseBlock', name: 'anyTool', toolUseId: 'tool-1', input: { x: 1 } })
        .addTurn({ type: 'textBlock', text: 'Done' })

      let toolExecuted = false
      const tool = createMockTool('anyTool', () => {
        toolExecuted = true
        return 'result'
      })

      const agent = new Agent({
        model,
        tools: [tool],
        interventions: [new HumanInTheLoop()],
        printer: false,
      })

      const result = await agent.invoke('Do something')

      expect(result.stopReason).toBe('interrupt')
      expect(result.interrupts).toEqual([
        expect.objectContaining({
          name: 'strands:human-in-the-loop',
          reason: expect.stringContaining('anyTool'),
        }),
      ])
      expect(toolExecuted).toBe(false)
    })
  })

  describe('inline mode (with ask callback)', () => {
    it('allows tool execution when approved', async () => {
      const model = new MockMessageModel()
        .addTurn({ type: 'toolUseBlock', name: 'myTool', toolUseId: 'tool-1', input: {} })
        .addTurn({ type: 'textBlock', text: 'Done' })

      let toolExecuted = false
      const tool = createMockTool('myTool', () => {
        toolExecuted = true
        return 'executed'
      })

      const agent = new Agent({
        model,
        tools: [tool],
        interventions: [new HumanInTheLoop({ ask: async () => 'yes' })],
        printer: false,
      })

      const result = await agent.invoke('Run tool')
      expect(result.stopReason).toBe('endTurn')
      expect(toolExecuted).toBe(true)
    })

    it('denies tool execution when rejected', async () => {
      const model = new MockMessageModel()
        .addTurn({ type: 'toolUseBlock', name: 'myTool', toolUseId: 'tool-1', input: {} })
        .addTurn({ type: 'textBlock', text: 'Understood' })

      let toolExecuted = false
      const tool = createMockTool('myTool', () => {
        toolExecuted = true
        return 'executed'
      })

      const agent = new Agent({
        model,
        tools: [tool],
        interventions: [new HumanInTheLoop({ ask: async () => 'no' })],
        printer: false,
      })

      const result = await agent.invoke('Run tool')
      expect(result.stopReason).toBe('endTurn')
      expect(toolExecuted).toBe(false)
    })
  })

  describe('allowedTools config', () => {
    it('does not prompt for tools in allowedTools', async () => {
      const model = new MockMessageModel()
        .addTurn({ type: 'toolUseBlock', name: 'readFile', toolUseId: 'tool-1', input: {} })
        .addTurn({ type: 'textBlock', text: 'Done' })

      let toolExecuted = false
      const tool = createMockTool('readFile', () => {
        toolExecuted = true
        return 'content'
      })

      const agent = new Agent({
        model,
        tools: [tool],
        interventions: [new HumanInTheLoop({ allowedTools: ['readFile'], ask: async () => 'no' })],
        printer: false,
      })

      const result = await agent.invoke('Read it')
      expect(result.stopReason).toBe('endTurn')
      expect(toolExecuted).toBe(true)
    })

    it('prompts for tools not in allowedTools', async () => {
      const model = new MockMessageModel()
        .addTurn({ type: 'toolUseBlock', name: 'deleteFile', toolUseId: 'tool-1', input: {} })
        .addTurn({ type: 'textBlock', text: 'Done' })

      let toolExecuted = false
      const tool = createMockTool('deleteFile', () => {
        toolExecuted = true
        return 'deleted'
      })

      const agent = new Agent({
        model,
        tools: [tool],
        interventions: [new HumanInTheLoop({ allowedTools: ['readFile'], ask: async () => 'no' })],
        printer: false,
      })

      await agent.invoke('Delete it')
      expect(toolExecuted).toBe(false)
    })

    it('allows all tools except negated ones with "!" prefix', async () => {
      const model = new MockMessageModel()
        .addTurn([
          { type: 'toolUseBlock', name: 'readFile', toolUseId: 'tool-1', input: {} },
          { type: 'toolUseBlock', name: 'deleteFile', toolUseId: 'tool-2', input: {} },
        ])
        .addTurn({ type: 'textBlock', text: 'Done' })

      const execLog: string[] = []
      const readTool = createMockTool('readFile', () => {
        execLog.push('read')
        return 'content'
      })
      const deleteTool = createMockTool('deleteFile', () => {
        execLog.push('delete')
        return 'deleted'
      })

      const agent = new Agent({
        model,
        tools: [readTool, deleteTool],
        interventions: [new HumanInTheLoop({ allowedTools: ['*', '!deleteFile'], ask: async () => 'no' })],
        printer: false,
      })

      await agent.invoke('Do both')

      expect(execLog).toContain('read')
      expect(execLog).not.toContain('delete')
    })

    it('allows all tools with wildcard "*"', async () => {
      const model = new MockMessageModel()
        .addTurn({ type: 'toolUseBlock', name: 'dangerousTool', toolUseId: 'tool-1', input: {} })
        .addTurn({ type: 'textBlock', text: 'Done' })

      let toolExecuted = false
      const tool = createMockTool('dangerousTool', () => {
        toolExecuted = true
        return 'ran'
      })

      const agent = new Agent({
        model,
        tools: [tool],
        interventions: [new HumanInTheLoop({ allowedTools: ['*'], ask: async () => 'no' })],
        printer: false,
      })

      const result = await agent.invoke('Do it')
      expect(result.stopReason).toBe('endTurn')
      expect(toolExecuted).toBe(true)
    })
  })

  describe('ask callback', () => {
    it('passes tool name and input in the prompt', async () => {
      const prompts: string[] = []

      const model = new MockMessageModel()
        .addTurn({ type: 'toolUseBlock', name: 'sendEmail', toolUseId: 'tool-1', input: { to: 'bob@example.com' } })
        .addTurn({ type: 'textBlock', text: 'Done' })

      const tool = createMockTool('sendEmail', () => 'sent')

      const agent = new Agent({
        model,
        tools: [tool],
        interventions: [
          new HumanInTheLoop({
            ask: async (prompt) => {
              prompts.push(prompt)
              return 'yes'
            },
          }),
        ],
        printer: false,
      })

      await agent.invoke('Send email')

      expect(prompts[0]).toContain('sendEmail')
      expect(prompts[0]).toContain('bob@example.com')
    })

    it('supports custom evaluate function', async () => {
      const model = new MockMessageModel()
        .addTurn({ type: 'toolUseBlock', name: 'myTool', toolUseId: 'tool-1', input: {} })
        .addTurn({ type: 'textBlock', text: 'Done' })

      let toolExecuted = false
      const tool = createMockTool('myTool', () => {
        toolExecuted = true
        return 'executed'
      })

      const agent = new Agent({
        model,
        tools: [tool],
        interventions: [
          new HumanInTheLoop({
            ask: async () => 'magic-word',
            evaluate: (response) => response === 'magic-word',
          }),
        ],
        printer: false,
      })

      const result = await agent.invoke('Go')
      expect(result.stopReason).toBe('endTurn')
      expect(toolExecuted).toBe(true)
    })
  })

  describe('trust mode (enableTrust: true)', () => {
    it('trusts a tool for the session when response is "t"', async () => {
      let askCount = 0

      const model = new MockMessageModel()
        .addTurn({ type: 'toolUseBlock', name: 'myTool', toolUseId: 'tool-1', input: {} })
        .addTurn({ type: 'toolUseBlock', name: 'myTool', toolUseId: 'tool-2', input: {} })
        .addTurn({ type: 'textBlock', text: 'Done' })

      const tool = createMockTool('myTool', () => 'executed')

      const agent = new Agent({
        model,
        tools: [tool],
        interventions: [
          new HumanInTheLoop({
            enableTrust: true,
            ask: async () => {
              askCount++
              return 't'
            },
          }),
        ],
        printer: false,
      })

      await agent.invoke('Run tool twice')

      expect(askCount).toBe(1)
    })

    it('does not trust when enableTrust is false even with "t" response', async () => {
      let askCount = 0

      const model = new MockMessageModel()
        .addTurn({ type: 'toolUseBlock', name: 'myTool', toolUseId: 'tool-1', input: {} })
        .addTurn({ type: 'toolUseBlock', name: 'myTool', toolUseId: 'tool-2', input: {} })
        .addTurn({ type: 'textBlock', text: 'Done' })

      const tool = createMockTool('myTool', () => 'executed')

      const agent = new Agent({
        model,
        tools: [tool],
        interventions: [
          new HumanInTheLoop({
            enableTrust: false,
            ask: async () => {
              askCount++
              return 't'
            },
          }),
        ],
        printer: false,
      })

      await agent.invoke('Run tool twice')

      // 't' is not recognized as approval when trust is disabled, so tool is denied both times
      // but ask is still called both times (no trust memory)
      expect(askCount).toBe(2)
    })

    it('"t" response also approves the current tool call', async () => {
      const model = new MockMessageModel()
        .addTurn({ type: 'toolUseBlock', name: 'myTool', toolUseId: 'tool-1', input: {} })
        .addTurn({ type: 'textBlock', text: 'Done' })

      let toolExecuted = false
      const tool = createMockTool('myTool', () => {
        toolExecuted = true
        return 'executed'
      })

      const agent = new Agent({
        model,
        tools: [tool],
        interventions: [new HumanInTheLoop({ enableTrust: true, ask: async () => 't' })],
        printer: false,
      })

      const result = await agent.invoke('Run tool')
      expect(result.stopReason).toBe('endTurn')
      expect(toolExecuted).toBe(true)
    })

    it.each(['trust', 'T', 'TRUST'])('trusts when response is "%s"', async (trustResponse) => {
      let askCount = 0

      const model = new MockMessageModel()
        .addTurn({ type: 'toolUseBlock', name: 'myTool', toolUseId: 'tool-1', input: {} })
        .addTurn({ type: 'toolUseBlock', name: 'myTool', toolUseId: 'tool-2', input: {} })
        .addTurn({ type: 'textBlock', text: 'Done' })

      const tool = createMockTool('myTool', () => 'executed')

      const agent = new Agent({
        model,
        tools: [tool],
        interventions: [
          new HumanInTheLoop({
            enableTrust: true,
            ask: async () => {
              askCount++
              return trustResponse
            },
          }),
        ],
        printer: false,
      })

      await agent.invoke('Run tool twice')
      expect(askCount).toBe(1)
    })

    it('supports custom evaluateTrust function', async () => {
      let askCount = 0

      const model = new MockMessageModel()
        .addTurn({ type: 'toolUseBlock', name: 'myTool', toolUseId: 'tool-1', input: {} })
        .addTurn({ type: 'toolUseBlock', name: 'myTool', toolUseId: 'tool-2', input: {} })
        .addTurn({ type: 'textBlock', text: 'Done' })

      const tool = createMockTool('myTool', () => 'executed')

      const agent = new Agent({
        model,
        tools: [tool],
        interventions: [
          new HumanInTheLoop({
            enableTrust: true,
            evaluateTrust: (r) => r === 'approve-and-remember',
            ask: async () => {
              askCount++
              return 'approve-and-remember'
            },
          }),
        ],
        printer: false,
      })

      await agent.invoke('Run tool twice')
      expect(askCount).toBe(1)
    })

    it('negated tools cannot be trusted even with "t" response', async () => {
      let askCount = 0
      let toolExecuted = false

      const model = new MockMessageModel()
        .addTurn({ type: 'toolUseBlock', name: 'dangerTool', toolUseId: 'tool-1', input: {} })
        .addTurn({ type: 'toolUseBlock', name: 'dangerTool', toolUseId: 'tool-2', input: {} })
        .addTurn({ type: 'textBlock', text: 'Done' })

      const tool = createMockTool('dangerTool', () => {
        toolExecuted = true
        return 'ran'
      })

      const agent = new Agent({
        model,
        tools: [tool],
        interventions: [
          new HumanInTheLoop({
            allowedTools: ['*', '!dangerTool'],
            enableTrust: true,
            ask: async () => {
              askCount++
              return 't'
            },
          }),
        ],
        printer: false,
      })

      await agent.invoke('Run danger twice')
      expect(askCount).toBe(2)
      expect(toolExecuted).toBe(false)
    })
  })
})
