/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig, normalizePath } from './test-helper.js';
import { join } from 'node:path';
import { writeFileSync } from 'node:fs';

describe('Hooks Agent Flow', () => {
  let rig: TestRig;

  beforeEach(() => {
    rig = new TestRig();
  });

  afterEach(async () => {
    if (rig) {
      await rig.cleanup();
    }
  });

  describe('BeforeAgent Hooks', () => {
    it('should inject additional context via BeforeAgent hook', async () => {
      await rig.setup('should inject additional context via BeforeAgent hook', {
        fakeResponsesPath: join(
          import.meta.dirname,
          'hooks-agent-flow.responses',
        ),
      });

      const hookScript = `
      try {
        const output = {
          decision: "allow",
          hookSpecificOutput: {
            hookEventName: "BeforeAgent",
            additionalContext: "SYSTEM INSTRUCTION: This is injected context."
          }
        };
        process.stdout.write(JSON.stringify(output));
      } catch (e) {
        console.error('Failed to write stdout:', e);
        process.exit(1);
      }
      console.error('DEBUG: BeforeAgent hook executed');
      `;

      const scriptPath = join(rig.testDir!, 'before_agent_context.cjs');
      writeFileSync(scriptPath, hookScript);

      await rig.setup('should inject additional context via BeforeAgent hook', {
        settings: {
          hooksConfig: {
            enabled: true,
          },
          hooks: {
            BeforeAgent: [
              {
                hooks: [
                  {
                    type: 'command',
                    command: `node "${scriptPath}"`,
                    timeout: 5000,
                  },
                ],
              },
            ],
          },
        },
      });

      await rig.run({ args: 'Hello test' });

      // Verify hook execution and telemetry
      const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
      expect(hookTelemetryFound).toBeTruthy();

      const hookLogs = rig.readHookLogs();
      const beforeAgentLog = hookLogs.find(
        (log) => log.hookCall.hook_event_name === 'BeforeAgent',
      );

      expect(beforeAgentLog).toBeDefined();
      expect(beforeAgentLog?.hookCall.stdout).toContain('injected context');
      expect(beforeAgentLog?.hookCall.stdout).toContain('"decision":"allow"');
      expect(beforeAgentLog?.hookCall.stdout).toContain(
        'SYSTEM INSTRUCTION: This is injected context.',
      );
    });
  });

  describe('AfterAgent Hooks', () => {
    it('should receive prompt and response in AfterAgent hook', async () => {
      await rig.setup('should receive prompt and response in AfterAgent hook', {
        fakeResponsesPath: join(
          import.meta.dirname,
          'hooks-agent-flow.responses',
        ),
      });

      const hookScript = `
      const fs = require('fs');
      try {
        const input = fs.readFileSync(0, 'utf-8');
        console.error('DEBUG: AfterAgent hook input received');
        process.stdout.write("Received Input: " + input);
      } catch (err) {
        console.error('Hook Failed:', err);
        process.exit(1);
      }
      `;

      const scriptPath = rig.createScript('after_agent_verify.cjs', hookScript);

      rig.setup('should receive prompt and response in AfterAgent hook', {
        settings: {
          hooksConfig: {
            enabled: true,
          },
          hooks: {
            AfterAgent: [
              {
                hooks: [
                  {
                    type: 'command',
                    command: normalizePath(`node "${scriptPath}"`)!,
                    timeout: 5000,
                  },
                ],
              },
            ],
          },
        },
      });

      await rig.run({ args: 'Hello validation' });

      const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
      expect(hookTelemetryFound).toBeTruthy();

      const hookLogs = rig.readHookLogs();
      const afterAgentLog = hookLogs.find(
        (log) => log.hookCall.hook_event_name === 'AfterAgent',
      );

      expect(afterAgentLog).toBeDefined();
      // Verify the hook stdout contains the input we echoed which proves the
      // hook received the prompt and response
      expect(afterAgentLog?.hookCall.stdout).toContain('Received Input');
      expect(afterAgentLog?.hookCall.stdout).toContain('Hello validation');
      // The fake response contains "Hello World"
      expect(afterAgentLog?.hookCall.stdout).toContain('Hello World');
    });

    it('should process clearContext in AfterAgent hook output', async () => {
      rig.setup('should process clearContext in AfterAgent hook output', {
        fakeResponsesPath: join(
          import.meta.dirname,
          'hooks-system.after-agent.responses',
        ),
      });

      // BeforeModel hook to track message counts across LLM calls
      const messageCountFile = join(rig.testDir!, 'message-counts.json');
      const escapedPath = JSON.stringify(messageCountFile);
      const beforeModelScript = `
        const fs = require('fs');
        const input = JSON.parse(fs.readFileSync(0, 'utf-8'));
        const messageCount = input.llm_request?.contents?.length || 0;
        let counts = [];
        try { counts = JSON.parse(fs.readFileSync(${escapedPath}, 'utf-8')); } catch (e) {}
        counts.push(messageCount);
        fs.writeFileSync(${escapedPath}, JSON.stringify(counts));
        console.log(JSON.stringify({ decision: 'allow' }));
      `;
      const beforeModelScriptPath = rig.createScript(
        'before_model_counter.cjs',
        beforeModelScript,
      );

      const afterAgentScript = `
        const fs = require('fs');
        const input = JSON.parse(fs.readFileSync(0, 'utf-8'));
        if (input.stop_hook_active) {
          // Retry turn: allow execution to proceed (breaks the loop)
          console.log(JSON.stringify({ decision: 'allow' }));
        } else {
          // First call: block and clear context to trigger the retry
          console.log(JSON.stringify({
            decision: 'block',
            reason: 'Security policy triggered',
            hookSpecificOutput: {
              hookEventName: 'AfterAgent',
              clearContext: true
            }
          }));
        }
      `;
      const afterAgentScriptPath = rig.createScript(
        'after_agent_clear.cjs',
        afterAgentScript,
      );

      rig.setup('should process clearContext in AfterAgent hook output', {
        settings: {
          hooksConfig: {
            enabled: true,
          },
          hooks: {
            BeforeModel: [
              {
                hooks: [
                  {
                    type: 'command',
                    command: normalizePath(`node "${beforeModelScriptPath}"`)!,
                    timeout: 5000,
                  },
                ],
              },
            ],
            AfterAgent: [
              {
                hooks: [
                  {
                    type: 'command',
                    command: normalizePath(`node "${afterAgentScriptPath}"`)!,
                    timeout: 5000,
                  },
                ],
              },
            ],
          },
        },
      });

      const result = await rig.run({ args: 'Hello test' });

      const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
      expect(hookTelemetryFound).toBeTruthy();

      const hookLogs = rig.readHookLogs();
      const afterAgentLog = hookLogs.find(
        (log) => log.hookCall.hook_event_name === 'AfterAgent',
      );

      expect(afterAgentLog).toBeDefined();
      expect(afterAgentLog?.hookCall.stdout).toContain('clearContext');
      expect(afterAgentLog?.hookCall.stdout).toContain('true');
      expect(result).toContain('Security policy triggered');

      // Verify context was cleared: second call should not have more messages than first
      const countsRaw = rig.readFile('message-counts.json');
      const counts = JSON.parse(countsRaw) as number[];
      expect(counts.length).toBeGreaterThanOrEqual(2);
      expect(counts[1]).toBeLessThanOrEqual(counts[0]);
    });
  });

  describe('Multi-step Loops', () => {
    it('should fire BeforeAgent and AfterAgent exactly once per turn despite tool calls', async () => {
      await rig.setup(
        'should fire BeforeAgent and AfterAgent exactly once per turn despite tool calls',
        {
          fakeResponsesPath: join(
            import.meta.dirname,
            'hooks-agent-flow-multistep.responses',
          ),
        },
      );

      // Create script files for hooks
      const baPath = rig.createScript(
        'ba_fired.cjs',
        "console.log('BeforeAgent Fired');",
      );
      const aaPath = rig.createScript(
        'aa_fired.cjs',
        "console.log('AfterAgent Fired');",
      );

      await rig.setup(
        'should fire BeforeAgent and AfterAgent exactly once per turn despite tool calls',
        {
          settings: {
            hooksConfig: {
              enabled: true,
            },
            hooks: {
              BeforeAgent: [
                {
                  hooks: [
                    {
                      type: 'command',
                      command: normalizePath(`node "${baPath}"`)!,
                      timeout: 5000,
                    },
                  ],
                },
              ],
              AfterAgent: [
                {
                  hooks: [
                    {
                      type: 'command',
                      command: normalizePath(`node "${aaPath}"`)!,
                      timeout: 5000,
                    },
                  ],
                },
              ],
            },
          },
        },
      );

      await rig.run({ args: 'Do a multi-step task' });

      const hookLogs = rig.readHookLogs();
      const beforeAgentLogs = hookLogs.filter(
        (log) => log.hookCall.hook_event_name === 'BeforeAgent',
      );
      const afterAgentLogs = hookLogs.filter(
        (log) => log.hookCall.hook_event_name === 'AfterAgent',
      );

      expect(beforeAgentLogs).toHaveLength(1);

      expect(afterAgentLogs).toHaveLength(1);

      const afterAgentLog = afterAgentLogs[0];
      expect(afterAgentLog).toBeDefined();
      expect(afterAgentLog?.hookCall.stdout).toContain('AfterAgent Fired');
    });
  });
});
