/**
 * Integration tests for prompt reference resolution.
 *
 * Tests the {{ref:prompt_id}} template syntax for modular prompt composition.
 * Uses real PromptReferenceResolver with mocked dependencies.
 */

import { describe, beforeEach, expect, test, jest } from '@jest/globals';

import { PromptReferenceResolver } from '../../../src/engine/execution/reference/prompt-reference-resolver.js';
import {
  CircularReferenceError,
  MaxDepthExceededError,
  PromptNotFoundError,
} from '../../../src/engine/execution/reference/errors.js';
import { processTemplateWithRefs } from '../../../src/shared/utils/jsonUtils.js';

import type { ConvertedPrompt } from '../../../src/engine/execution/types.js';
import type { Logger } from '../../../src/infra/logging/index.js';
import type {
  LoadedScriptTool,
  ScriptExecutionResult,
  ToolDetectionMatch,
} from '../../../src/modules/automation/types.js';
import type {
  ToolDetectionServicePort,
  ScriptExecutorPort,
} from '../../../src/engine/execution/reference/prompt-reference-resolver.js';

describe('PromptReferenceResolver', () => {
  let resolver: PromptReferenceResolver;
  let mockLogger: Logger;
  let prompts: ConvertedPrompt[];

  // Helper to create mock prompts
  const createPrompt = (
    id: string,
    template: string,
    overrides: Partial<ConvertedPrompt> = {}
  ): ConvertedPrompt => ({
    id,
    name: `${id} Prompt`,
    description: `Test prompt ${id}`,
    category: 'test',
    userMessageTemplate: template,
    arguments: [],
    ...overrides,
  });

  beforeEach(() => {
    mockLogger = {
      info: jest.fn(),
      error: jest.fn(),
      warn: jest.fn(),
      debug: jest.fn(),
    };

    // Create test prompts
    prompts = [
      createPrompt('intro', 'Welcome to {{topic}}!'),
      createPrompt('shared_header', '# Header: {{title}}'),
      createPrompt('with_ref', 'Start: {{ref:intro}} End.'),
      createPrompt('nested_ref', '{{ref:with_ref}} -- More content'),
      createPrompt('circular_a', '{{ref:circular_b}} from A'),
      createPrompt('circular_b', '{{ref:circular_a}} from B'),
      createPrompt('multi_ref', '{{ref:intro}} | {{ref:shared_header}}'),
      createPrompt('deep_1', '{{ref:deep_2}}'),
      createPrompt('deep_2', '{{ref:deep_3}}'),
      createPrompt('deep_3', '{{ref:deep_4}}'),
      createPrompt('deep_4', '{{ref:deep_5}}'),
      createPrompt('deep_5', 'Finally!'),
    ];

    resolver = new PromptReferenceResolver(mockLogger, prompts);
  });

  describe('detectReferences', () => {
    test('detects single reference', () => {
      const refs = resolver.detectReferences('Hello {{ref:intro}} world');
      expect(refs).toHaveLength(1);
      expect(refs[0]?.promptId).toBe('intro');
      expect(refs[0]?.fullMatch).toBe('{{ref:intro}}');
    });

    test('detects multiple references', () => {
      const refs = resolver.detectReferences('{{ref:intro}} and {{ref:shared_header}}');
      expect(refs).toHaveLength(2);
      expect(refs[0]?.promptId).toBe('intro');
      expect(refs[1]?.promptId).toBe('shared_header');
    });

    test('returns empty array for no references', () => {
      const refs = resolver.detectReferences('No references here {{normal}}');
      expect(refs).toHaveLength(0);
    });

    test('supports prompt IDs with hyphens and underscores', () => {
      const refs = resolver.detectReferences('{{ref:my-prompt_v2}}');
      expect(refs).toHaveLength(1);
      expect(refs[0]?.promptId).toBe('my-prompt_v2');
    });
  });

  describe('hasReferences', () => {
    test('returns true when template contains references', () => {
      expect(resolver.hasReferences('{{ref:intro}}')).toBe(true);
    });

    test('returns false when template has no references', () => {
      expect(resolver.hasReferences('{{normal}} variable')).toBe(false);
    });
  });

  describe('resolveReference', () => {
    test('resolves simple reference with context', async () => {
      const result = await resolver.resolveReference('intro', { topic: 'Testing' });

      expect(result.content).toBe('Welcome to Testing!');
      expect(result.resolvedPromptIds.has('intro')).toBe(true);
      expect(result.resolutionChain).toEqual(['intro']);
    });

    test('throws PromptNotFoundError for unknown prompt', async () => {
      await expect(resolver.resolveReference('unknown', {})).rejects.toThrow(PromptNotFoundError);
    });

    test('resolves nested references', async () => {
      const result = await resolver.resolveReference('nested_ref', { topic: 'Nesting' });

      // nested_ref -> with_ref -> intro
      expect(result.content).toContain('Welcome to Nesting!');
      expect(result.resolvedPromptIds.has('nested_ref')).toBe(true);
      expect(result.resolvedPromptIds.has('with_ref')).toBe(true);
      expect(result.resolvedPromptIds.has('intro')).toBe(true);
    });

    test('throws CircularReferenceError for circular references', async () => {
      await expect(resolver.resolveReference('circular_a', {})).rejects.toThrow(
        CircularReferenceError
      );
    });

    test('includes circular chain in error', async () => {
      try {
        await resolver.resolveReference('circular_a', {});
        expect.fail('Should have thrown');
      } catch (error) {
        expect(error).toBeInstanceOf(CircularReferenceError);
        expect((error as CircularReferenceError).chain).toContain('circular_a');
        expect((error as CircularReferenceError).chain).toContain('circular_b');
      }
    });
  });

  describe('preResolve', () => {
    test('resolves all references in template', async () => {
      const result = await resolver.preResolve('Before {{ref:intro}} After', { topic: 'World' });

      expect(result.resolvedTemplate).toBe('Before Welcome to World! After');
      expect(result.diagnostics.referencesResolved).toBe(1);
    });

    test('handles multiple references in same template', async () => {
      const result = await resolver.preResolve('{{ref:intro}} | {{ref:shared_header}}', {
        topic: 'Multi',
        title: 'Test',
      });

      expect(result.resolvedTemplate).toBe('Welcome to Multi! | # Header: Test');
      expect(result.diagnostics.referencesResolved).toBe(2);
    });

    test('returns unchanged template when no references', async () => {
      const template = 'No refs: {{variable}}';
      const result = await resolver.preResolve(template, {});

      expect(result.resolvedTemplate).toBe(template);
      expect(result.diagnostics.referencesResolved).toBe(0);
    });

    test('handles deeply nested references', async () => {
      const result = await resolver.preResolve('{{ref:deep_1}}', {});

      expect(result.resolvedTemplate).toBe('Finally!');
      expect(result.resolvedPromptIds.size).toBe(5);
    });

    test('respects maxDepth option', async () => {
      const shallowResolver = new PromptReferenceResolver(
        mockLogger,
        prompts,
        undefined,
        undefined,
        {
          maxDepth: 3,
        }
      );

      await expect(shallowResolver.preResolve('{{ref:deep_1}}', {})).rejects.toThrow(
        MaxDepthExceededError
      );
    });

    test('tracks resolution time in diagnostics', async () => {
      const result = await resolver.preResolve('{{ref:intro}}', { topic: 'Time' });

      expect(result.diagnostics.resolutionTimeMs).toBeGreaterThanOrEqual(0);
    });
  });

  describe('with throwOnMissing: false', () => {
    test('replaces missing references with empty string and logs warning', async () => {
      const lenientResolver = new PromptReferenceResolver(
        mockLogger,
        prompts,
        undefined,
        undefined,
        {
          throwOnMissing: false,
        }
      );

      const result = await lenientResolver.preResolve('Start {{ref:unknown}} End', {});

      expect(result.resolvedTemplate).toBe('Start  End');
      expect(result.diagnostics.warnings).toContain('Reference to unknown prompt: unknown');
    });
  });
});

describe('processTemplateWithRefs', () => {
  let mockLogger: Logger;
  let prompts: ConvertedPrompt[];

  const createPrompt = (id: string, template: string): ConvertedPrompt => ({
    id,
    name: `${id} Prompt`,
    description: `Test prompt ${id}`,
    category: 'test',
    userMessageTemplate: template,
    arguments: [],
  });

  beforeEach(() => {
    mockLogger = {
      info: jest.fn(),
      error: jest.fn(),
      warn: jest.fn(),
      debug: jest.fn(),
    };

    prompts = [
      createPrompt('greeting', 'Hello, {{name}}!'),
      createPrompt('with_greeting', '{{ref:greeting}} Welcome to {{place}}.'),
    ];
  });

  test('processes template without resolver (standard Nunjucks)', async () => {
    const result = await processTemplateWithRefs('Hello, {{name}}!', { name: 'World' });

    expect(result.content).toBe('Hello, World!');
    expect(result.resolvedPromptIds.size).toBe(0);
  });

  test('processes template with resolver for reference resolution', async () => {
    const resolver = new PromptReferenceResolver(mockLogger, prompts);

    const result = await processTemplateWithRefs(
      '{{ref:greeting}} Goodbye.',
      { name: 'Alice' },
      {},
      resolver
    );

    expect(result.content).toBe('Hello, Alice! Goodbye.');
    expect(result.resolvedPromptIds.has('greeting')).toBe(true);
  });

  test('merges context across reference and parent template', async () => {
    const resolver = new PromptReferenceResolver(mockLogger, prompts);

    const result = await processTemplateWithRefs(
      '{{ref:with_greeting}}',
      { name: 'Bob', place: 'Wonderland' },
      {},
      resolver
    );

    expect(result.content).toBe('Hello, Bob! Welcome to Wonderland.');
  });
});

describe('PromptReferenceResolver with scripts', () => {
  let mockLogger: Logger;
  let prompts: ConvertedPrompt[];
  let mockToolDetection: jest.Mocked<ToolDetectionServicePort>;
  let mockScriptExecutor: jest.Mocked<ScriptExecutorPort>;

  const createScriptTool = (id: string): LoadedScriptTool => ({
    id,
    name: `${id} Tool`,
    description: 'Test tool',
    scriptPath: 'script.py',
    inputSchema: { type: 'object' },
    toolDir: '/test/tools',
    absoluteScriptPath: '/test/tools/script.py',
    promptId: 'with_script',
    descriptionContent: 'Test tool description',
  });

  beforeEach(() => {
    mockLogger = {
      info: jest.fn(),
      error: jest.fn(),
      warn: jest.fn(),
      debug: jest.fn(),
    };

    const scriptTool = createScriptTool('analyzer');

    prompts = [
      {
        id: 'with_script',
        name: 'With Script',
        description: 'Prompt with script',
        category: 'test',
        userMessageTemplate: 'Result: {{tool_analyzer}}',
        arguments: [],
        scriptTools: [scriptTool],
      } as ConvertedPrompt,
      {
        id: 'refs_script',
        name: 'Refs Script',
        description: 'References prompt with script',
        category: 'test',
        userMessageTemplate: '{{ref:with_script}}',
        arguments: [],
      } as ConvertedPrompt,
    ];

    mockToolDetection = {
      detectTools: jest.fn().mockReturnValue([
        {
          toolId: 'analyzer',
          promptId: 'with_script',
          priority: 1.0,
          matchReason: 'always_match',
          extractedInputs: { data: 'test' },
          requiresConfirmation: false,
        } as ToolDetectionMatch,
      ]),
    } as unknown as jest.Mocked<ToolDetectionServicePort>;

    mockScriptExecutor = {
      execute: jest.fn().mockResolvedValue({
        success: true,
        output: 'Script output!',
        stdout: 'Script output!',
        stderr: '',
        exitCode: 0,
        durationMs: 100,
      } as ScriptExecutionResult),
    } as unknown as jest.Mocked<ScriptExecutorPort>;
  });

  test('executes scripts when resolving reference with scriptTools', async () => {
    const resolver = new PromptReferenceResolver(
      mockLogger,
      prompts,
      mockToolDetection,
      mockScriptExecutor,
      { executeScripts: true }
    );

    const result = await resolver.resolveReference('with_script', { data: 'test' });

    expect(mockScriptExecutor.execute).toHaveBeenCalled();
    expect(result.scriptResults.has('analyzer')).toBe(true);
    expect(result.content).toBe('Result: Script output!');
  });

  test('makes script results available in template context', async () => {
    const resolver = new PromptReferenceResolver(
      mockLogger,
      prompts,
      mockToolDetection,
      mockScriptExecutor
    );

    const result = await resolver.preResolve('{{ref:with_script}}', { data: 'test' });

    expect(result.scriptResults.has('with_script:analyzer')).toBe(true);
  });

  test('skips script execution when executeScripts is false', async () => {
    const resolver = new PromptReferenceResolver(
      mockLogger,
      prompts,
      mockToolDetection,
      mockScriptExecutor,
      { executeScripts: false }
    );

    await resolver.resolveReference('with_script', {});

    expect(mockScriptExecutor.execute).not.toHaveBeenCalled();
  });

  test('skips tools requiring confirmation', async () => {
    mockToolDetection.detectTools.mockReturnValue([
      {
        toolId: 'analyzer',
        promptId: 'with_script',
        priority: 1.0,
        matchReason: 'schema_match',
        extractedInputs: {},
        requiresConfirmation: true, // Requires confirmation
      } as ToolDetectionMatch,
    ]);

    const resolver = new PromptReferenceResolver(
      mockLogger,
      prompts,
      mockToolDetection,
      mockScriptExecutor
    );

    const result = await resolver.resolveReference('with_script', {});

    expect(mockScriptExecutor.execute).not.toHaveBeenCalled();
    expect(result.scriptResults.size).toBe(0);
  });
});
