/**
 * Contract test: assembled prompts from PromptFactory.buildPrompt() and
 * InitRunner.build*Prompt() must contain zero interactive patterns.
 *
 * Unlike headless-prompts.test.ts (which scans raw .md files on disk),
 * these tests exercise the full assembly pipeline:
 *   file loading → role extraction → context injection → sanitizePrompt()
 *
 * If any assembly step reintroduces interactive patterns that sanitizePrompt()
 * doesn't catch, these tests will fail.
 */
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';

import { PromptFactory } from './phase-prompt.js';
import { InitRunner } from './init-runner.js';
import { PhaseType } from './types.js';
import type { ParsedPlan, ContextFiles, GSDEvent } from './types.js';
import type { GSDTools } from './gsd-tools.js';
import type { GSDEventStream } from './event-stream.js';

// ─── Paths ───────────────────────────────────────────────────────────────────

const __dirname = dirname(fileURLToPath(import.meta.url));
const sdkPromptsDir = join(__dirname, '..', 'prompts');

// ─── Blocked patterns (aligned with headless-prompts.test.ts) ────────────────

const BLOCKED_PATTERNS: Array<[string, RegExp]> = [
  ['AskUserQuestion', /AskUserQuestion\s*\(/],
  ['SlashCommand', /SlashCommand\s*\(/],
  ['/gsd: command', /\/gsd:\S+/],
  ['@file: reference', /@file:\S+/],
  ['STOP + wait directive', /\bSTOP\b\s+(?:and\s+)?(?:wait|ask)/i],
  ['bare STOP directive', /^\s*STOP\s*[.!]?\s*$/m],
  ['wait for user', /\bwait\s+for\s+(?:the\s+)?user\b/i],
  ['ask the user', /\bask\s+the\s+user\b/i],
];

// ─── Minimal fixtures ────────────────────────────────────────────────────────

const MINIMAL_PLAN: ParsedPlan = {
  frontmatter: {
    phase: '01',
    plan: 'test-plan',
    type: 'feature',
    wave: 1,
    depends_on: [],
    files_modified: ['src/index.ts'],
    autonomous: true,
    requirements: ['R001'],
    must_haves: {
      truths: ['It works'],
      artifacts: [{ path: 'src/index.ts', provides: 'entry point' }],
      key_links: [],
    },
  },
  objective: 'Test objective for assembled prompt contract test',
  execution_context: ['This is a test context line'],
  context_refs: [],
  tasks: [
    {
      type: 'create',
      name: 'Create test file',
      files: ['src/test.ts'],
      read_first: [],
      action: 'Create a test file',
      verify: 'File exists',
      acceptance_criteria: ['File created'],
      done: 'src/test.ts exists',
    },
  ],
  raw: '# Test Plan\n\nMinimal plan for testing.',
};

const EMPTY_CONTEXT: ContextFiles = {};

// ─── Helper ──────────────────────────────────────────────────────────────────

function assertNoBlockedPatterns(output: string, label: string): void {
  for (const [patternLabel, pattern] of BLOCKED_PATTERNS) {
    const matches = output.match(new RegExp(pattern.source, pattern.flags + 'g'));
    expect(
      matches,
      `Found ${patternLabel} in ${label}: ${matches?.join(', ')}`,
    ).toBeNull();
  }
}

// ─── PromptFactory assembled output ──────────────────────────────────────────

describe('PromptFactory assembled output', () => {
  let factory: PromptFactory;

  beforeAll(() => {
    factory = new PromptFactory({ sdkPromptsDir });
  });

  const phaseTypes = Object.values(PhaseType) as PhaseType[];

  for (const phaseType of phaseTypes) {
    describe(`${phaseType} phase`, () => {
      let output: string;

      beforeAll(async () => {
        output = await factory.buildPrompt(phaseType, MINIMAL_PLAN, EMPTY_CONTEXT);
      });

      it('produces non-empty output', () => {
        expect(output.length).toBeGreaterThan(0);
      });

      for (const [label, pattern] of BLOCKED_PATTERNS) {
        it(`contains no ${label}`, () => {
          const matches = output.match(new RegExp(pattern.source, pattern.flags + 'g'));
          expect(
            matches,
            `Found ${label} in ${phaseType} assembled prompt: ${matches?.join(', ')}`,
          ).toBeNull();
        });
      }
    });
  }

  it('includes role section for phases with agents', async () => {
    // Research, Plan, Execute, Verify all have agents; Discuss does not
    const researchOutput = await factory.buildPrompt(PhaseType.Research, null, EMPTY_CONTEXT);
    expect(researchOutput).toContain('## Role');
  });

  it('includes purpose section from workflow files', async () => {
    const planOutput = await factory.buildPrompt(PhaseType.Plan, null, EMPTY_CONTEXT);
    // Plan phase should have purpose from plan-phase.md
    expect(planOutput).toContain('## Purpose');
  });

  it('includes context section when context files provided', async () => {
    const contextFiles: ContextFiles = {
      state: '# State\ncurrent_phase: 01',
      roadmap: '# Roadmap\n## Phase 01',
    };
    const output = await factory.buildPrompt(PhaseType.Research, null, contextFiles);
    expect(output).toContain('## Context');
    expect(output).toContain('Project State');
  });
});

// ─── InitRunner assembled output ─────────────────────────────────────────────

describe('InitRunner assembled output', () => {
  let tmpDir: string;
  let runner: InitRunner;

  // Minimal stub tools and event stream — we only call build*Prompt(), not run()
  const stubTools: GSDTools = {
    initNewProject: async () => ({
      researcher_model: 'test',
      synthesizer_model: 'test',
      roadmapper_model: 'test',
      commit_docs: false,
      project_exists: false,
      has_codebase_map: false,
      has_git: true,
    }),
    configSet: async () => {},
    commit: async () => {},
  } as unknown as GSDTools;

  const stubEventStream: GSDEventStream = {
    emitEvent: (_event: GSDEvent) => {},
  } as unknown as GSDEventStream;

  beforeAll(async () => {
    // Create temp directory with .planning/ structure for InitRunner file reads
    tmpDir = await mkdtemp(join(tmpdir(), 'assembled-prompts-'));
    const planningDir = join(tmpDir, '.planning');
    const researchDir = join(planningDir, 'research');
    await mkdir(researchDir, { recursive: true });

    // Write minimal stubs that InitRunner reads
    await writeFile(
      join(planningDir, 'PROJECT.md'),
      '# Test Project\n\nA minimal test project for contract testing.\n',
    );
    await writeFile(
      join(planningDir, 'config.json'),
      JSON.stringify({ mode: 'yolo', parallelization: true }, null, 2),
    );
    await writeFile(
      join(planningDir, 'REQUIREMENTS.md'),
      '# Requirements\n\n## R001 — Test Requirement\n',
    );
    await writeFile(
      join(researchDir, 'STACK.md'),
      '# Stack Research\n\nTypeScript + Node.js\n',
    );
    await writeFile(
      join(researchDir, 'FEATURES.md'),
      '# Features Research\n\nCore features identified.\n',
    );
    await writeFile(
      join(researchDir, 'ARCHITECTURE.md'),
      '# Architecture Research\n\nModular architecture.\n',
    );
    await writeFile(
      join(researchDir, 'PITFALLS.md'),
      '# Pitfalls Research\n\nCommon pitfalls noted.\n',
    );
    await writeFile(
      join(researchDir, 'SUMMARY.md'),
      '# Research Summary\n\nAll research synthesized.\n',
    );

    runner = new InitRunner({
      projectDir: tmpDir,
      tools: stubTools,
      eventStream: stubEventStream,
      sdkPromptsDir,
    });
  });

  afterAll(async () => {
    if (tmpDir) {
      await rm(tmpDir, { recursive: true, force: true });
    }
  });

  // Access private methods via (runner as any) — standard pattern for testing
  // private methods in TypeScript without subclassing or mocking

  describe('buildProjectPrompt', () => {
    let output: string;

    beforeAll(async () => {
      output = await (runner as any).buildProjectPrompt('Build a CLI tool');
    });

    it('produces non-empty output', () => {
      expect(output.length).toBeGreaterThan(0);
    });

    it('contains project template content', () => {
      expect(output).toContain('PROJECT.md');
    });

    it('contains user input', () => {
      expect(output).toContain('Build a CLI tool');
    });

    it('contains zero blocked patterns', () => {
      assertNoBlockedPatterns(output, 'buildProjectPrompt');
    });
  });

  describe('buildResearchPrompt', () => {
    const researchTypes = ['STACK', 'FEATURES', 'ARCHITECTURE', 'PITFALLS'] as const;

    for (const researchType of researchTypes) {
      describe(`${researchType} research`, () => {
        let output: string;

        beforeAll(async () => {
          output = await (runner as any).buildResearchPrompt(researchType, 'Build a CLI tool');
        });

        it('produces non-empty output', () => {
          expect(output.length).toBeGreaterThan(0);
        });

        it('references the research type', () => {
          expect(output).toContain(researchType);
        });

        it('contains zero blocked patterns', () => {
          assertNoBlockedPatterns(output, `buildResearchPrompt(${researchType})`);
        });
      });
    }
  });

  describe('buildSynthesisPrompt', () => {
    let output: string;

    beforeAll(async () => {
      output = await (runner as any).buildSynthesisPrompt();
    });

    it('produces non-empty output', () => {
      expect(output.length).toBeGreaterThan(0);
    });

    it('contains research content from temp files', () => {
      // The synthesis prompt reads research files from disk — our stubs should appear
      expect(output).toContain('Stack Research');
    });

    it('contains zero blocked patterns', () => {
      assertNoBlockedPatterns(output, 'buildSynthesisPrompt');
    });
  });

  describe('buildRequirementsPrompt', () => {
    let output: string;

    beforeAll(async () => {
      output = await (runner as any).buildRequirementsPrompt();
    });

    it('produces non-empty output', () => {
      expect(output.length).toBeGreaterThan(0);
    });

    it('contains project context from temp files', () => {
      expect(output).toContain('Test Project');
    });

    it('contains zero blocked patterns', () => {
      assertNoBlockedPatterns(output, 'buildRequirementsPrompt');
    });
  });

  describe('buildRoadmapPrompt', () => {
    let output: string;

    beforeAll(async () => {
      output = await (runner as any).buildRoadmapPrompt();
    });

    it('produces non-empty output', () => {
      expect(output.length).toBeGreaterThan(0);
    });

    it('contains agent definition content', () => {
      // Roadmap prompt loads gsd-roadmapper.md
      expect(output).toContain('agent_definition');
    });

    it('contains project file content', () => {
      expect(output).toContain('Test Project');
    });

    it('contains zero blocked patterns', () => {
      assertNoBlockedPatterns(output, 'buildRoadmapPrompt');
    });
  });
});
