/**
 * File-Based Agent I/O Tests
 */

import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest';
import { mkdirSync, writeFileSync, readFileSync, rmSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { randomUUID } from 'crypto';
import {
  writeInputFiles,
  readSummary,
  readPhaseFiles,
  readTaskFiles,
  readDecisionFiles,
  readPageFiles,
  generateId,
  writeErrandManifest,
} from './file-io.js';
import { buildErrandPrompt } from './prompts/index.js';
import type { Initiative, Phase, Task } from '../db/schema.js';

let testDir: string;

beforeEach(() => {
  testDir = join(tmpdir(), `cw-file-io-test-${randomUUID()}`);
  mkdirSync(testDir, { recursive: true });
});

afterEach(() => {
  rmSync(testDir, { recursive: true, force: true });
});

describe('generateId', () => {
  it('returns a non-empty string', () => {
    const id = generateId();
    expect(id).toBeTruthy();
    expect(typeof id).toBe('string');
  });

  it('returns unique values', () => {
    const ids = new Set(Array.from({ length: 100 }, () => generateId()));
    expect(ids.size).toBe(100);
  });
});

describe('writeInputFiles', () => {
  it('writes initiative.md with frontmatter', async () => {
    const initiative: Initiative = {
      id: 'init-1',
      name: 'Test Initiative',
      status: 'active',
      branch: 'cw/test-initiative',
      executionMode: 'review_per_phase',
      createdAt: new Date('2026-01-01'),
      updatedAt: new Date('2026-01-02'),
    };

    await writeInputFiles({ agentWorkdir: testDir, initiative });

    const filePath = join(testDir, '.cw', 'input', 'initiative.md');
    expect(existsSync(filePath)).toBe(true);
  });

  it('writes phase.md with frontmatter', async () => {
    const phase = {
      id: 'phase-1',
      initiativeId: 'init-1',
      number: 1,
      name: 'Phase One',
      content: 'First phase',
      status: 'pending',
      mergeBase: null,
      createdAt: new Date(),
      updatedAt: new Date(),
    } as Phase;

    await writeInputFiles({ agentWorkdir: testDir, phase });

    const filePath = join(testDir, '.cw', 'input', 'phase.md');
    expect(existsSync(filePath)).toBe(true);
  });

  it('writes task.md with frontmatter', async () => {
    const task = {
      id: 'task-1',
      name: 'Test Task',
      description: 'Do the thing',
      category: 'execute',
      type: 'auto',
      priority: 'medium',
      status: 'pending',
      order: 1,
      createdAt: new Date(),
      updatedAt: new Date(),
    } as Task;

    await writeInputFiles({ agentWorkdir: testDir, task });

    const filePath = join(testDir, '.cw', 'input', 'task.md');
    expect(existsSync(filePath)).toBe(true);
  });

  it('writes pages to pages/ subdirectory', async () => {
    await writeInputFiles({
      agentWorkdir: testDir,
      pages: [
        { id: 'page-1', parentPageId: null, title: 'Root', content: null, sortOrder: 0 },
        { id: 'page-2', parentPageId: 'page-1', title: 'Child', content: null, sortOrder: 1 },
      ],
    });

    expect(existsSync(join(testDir, '.cw', 'input', 'pages', 'page-1.md'))).toBe(true);
    expect(existsSync(join(testDir, '.cw', 'input', 'pages', 'page-2.md'))).toBe(true);
  });

  it('handles empty options without error', async () => {
    await writeInputFiles({ agentWorkdir: testDir });
    expect(existsSync(join(testDir, '.cw', 'input'))).toBe(true);
  });

  it('writes context/index.json grouping tasks by phaseId', async () => {
    await writeInputFiles({
      agentWorkdir: testDir,
      tasks: [
        { id: 't1', name: 'Task A', phaseId: 'ph1', status: 'pending', category: 'execute', type: 'auto', priority: 'medium' } as Task,
        { id: 't2', name: 'Task B', phaseId: 'ph1', status: 'completed', category: 'execute', type: 'auto', priority: 'medium', summary: 'Done' } as Task,
        { id: 't3', name: 'Task C', phaseId: 'ph2', status: 'pending', category: 'research', type: 'auto', priority: 'high' } as Task,
      ],
    });

    const indexPath = join(testDir, '.cw', 'input', 'context', 'index.json');
    expect(existsSync(indexPath)).toBe(true);
    const index = JSON.parse(readFileSync(indexPath, 'utf-8'));
    expect(index.tasksByPhase.ph1).toHaveLength(2);
    expect(index.tasksByPhase.ph2).toHaveLength(1);
    expect(index.tasksByPhase.ph1[0]).toEqual({
      file: 'context/tasks/t1.md',
      id: 't1',
      name: 'Task A',
      status: 'pending',
    });
  });

  it('does not write context/index.json when no tasks', async () => {
    await writeInputFiles({ agentWorkdir: testDir });
    expect(existsSync(join(testDir, '.cw', 'input', 'context', 'index.json'))).toBe(false);
  });
});

describe('readSummary', () => {
  it('reads SUMMARY.md with frontmatter', async () => {
    const outputDir = join(testDir, '.cw', 'output');
    mkdirSync(outputDir, { recursive: true });

    writeFileSync(
      join(outputDir, 'SUMMARY.md'),
      `---
files_modified:
  - src/foo.ts
  - src/bar.ts
---
Task completed successfully. Refactored the module.
`,
      'utf-8',
    );

    const summary = await readSummary(testDir);
    expect(summary).not.toBeNull();
    expect(summary!.body).toBe('Task completed successfully. Refactored the module.');
    expect(summary!.filesModified).toEqual(['src/foo.ts', 'src/bar.ts']);
  });

  it('returns null when SUMMARY.md does not exist', async () => {
    const summary = await readSummary(testDir);
    expect(summary).toBeNull();
  });

  it('handles SUMMARY.md without frontmatter', async () => {
    const outputDir = join(testDir, '.cw', 'output');
    mkdirSync(outputDir, { recursive: true });
    writeFileSync(join(outputDir, 'SUMMARY.md'), 'Just plain text\n', 'utf-8');

    const summary = await readSummary(testDir);
    expect(summary).not.toBeNull();
    expect(summary!.body).toBe('Just plain text');
    expect(summary!.filesModified).toBeUndefined();
  });

  it('handles empty files_modified', async () => {
    const outputDir = join(testDir, '.cw', 'output');
    mkdirSync(outputDir, { recursive: true });
    writeFileSync(
      join(outputDir, 'SUMMARY.md'),
      `---
files_modified: []
---
Done.
`,
      'utf-8',
    );

    const summary = await readSummary(testDir);
    expect(summary!.filesModified).toEqual([]);
  });
});

describe('readPhaseFiles', () => {
  it('reads phase files from phases/ directory', async () => {
    const phasesDir = join(testDir, '.cw', 'output', 'phases');
    mkdirSync(phasesDir, { recursive: true });

    writeFileSync(
      join(phasesDir, 'abc123.md'),
      `---
title: Database Schema
dependencies:
  - xyz789
---
Create the user tables and auth schema.
`,
      'utf-8',
    );

    const phases = await readPhaseFiles(testDir);
    expect(phases).toHaveLength(1);
    expect(phases[0].id).toBe('abc123');
    expect(phases[0].title).toBe('Database Schema');
    expect(phases[0].dependencies).toEqual(['xyz789']);
    expect(phases[0].body).toBe('Create the user tables and auth schema.');
  });

  it('returns empty array when directory does not exist', async () => {
    const phases = await readPhaseFiles(testDir);
    expect(phases).toEqual([]);
  });

  it('handles phases with no dependencies', async () => {
    const phasesDir = join(testDir, '.cw', 'output', 'phases');
    mkdirSync(phasesDir, { recursive: true });

    writeFileSync(
      join(phasesDir, 'p1.md'),
      `---
title: Foundation
---
Set up the base.
`,
      'utf-8',
    );

    const phases = await readPhaseFiles(testDir);
    expect(phases[0].dependencies).toEqual([]);
  });
});

describe('readTaskFiles', () => {
  it('reads task files from tasks/ directory', async () => {
    const tasksDir = join(testDir, '.cw', 'output', 'tasks');
    mkdirSync(tasksDir, { recursive: true });

    writeFileSync(
      join(tasksDir, 'task-1.md'),
      `---
title: Implement login
category: execute
type: auto
dependencies:
  - task-0
---
Build the login form and submit handler.
`,
      'utf-8',
    );

    const tasks = await readTaskFiles(testDir);
    expect(tasks).toHaveLength(1);
    expect(tasks[0].id).toBe('task-1');
    expect(tasks[0].title).toBe('Implement login');
    expect(tasks[0].category).toBe('execute');
    expect(tasks[0].type).toBe('auto');
    expect(tasks[0].dependencies).toEqual(['task-0']);
    expect(tasks[0].body).toBe('Build the login form and submit handler.');
  });

  it('defaults category and type when missing', async () => {
    const tasksDir = join(testDir, '.cw', 'output', 'tasks');
    mkdirSync(tasksDir, { recursive: true });
    writeFileSync(join(tasksDir, 't1.md'), `---\ntitle: Minimal\n---\nDo it.\n`, 'utf-8');

    const tasks = await readTaskFiles(testDir);
    expect(tasks[0].category).toBe('execute');
    expect(tasks[0].type).toBe('auto');
  });

  it('returns empty array when directory does not exist', async () => {
    expect(await readTaskFiles(testDir)).toEqual([]);
  });
});

describe('readDecisionFiles', () => {
  it('reads decision files from decisions/ directory', async () => {
    const decisionsDir = join(testDir, '.cw', 'output', 'decisions');
    mkdirSync(decisionsDir, { recursive: true });

    writeFileSync(
      join(decisionsDir, 'd1.md'),
      `---
topic: Authentication
decision: Use JWT
reason: Stateless and scalable
---
Additional context about the decision.
`,
      'utf-8',
    );

    const decisions = await readDecisionFiles(testDir);
    expect(decisions).toHaveLength(1);
    expect(decisions[0].id).toBe('d1');
    expect(decisions[0].topic).toBe('Authentication');
    expect(decisions[0].decision).toBe('Use JWT');
    expect(decisions[0].reason).toBe('Stateless and scalable');
    expect(decisions[0].body).toBe('Additional context about the decision.');
  });

  it('returns empty array when directory does not exist', async () => {
    expect(await readDecisionFiles(testDir)).toEqual([]);
  });
});

describe('readPageFiles', () => {
  it('reads page files from pages/ directory', async () => {
    const pagesDir = join(testDir, '.cw', 'output', 'pages');
    mkdirSync(pagesDir, { recursive: true });

    writeFileSync(
      join(pagesDir, 'page-abc.md'),
      `---
title: Architecture Overview
summary: Updated the overview section
---
# Architecture

New content for the page.
`,
      'utf-8',
    );

    const pages = await readPageFiles(testDir);
    expect(pages).toHaveLength(1);
    expect(pages[0].pageId).toBe('page-abc');
    expect(pages[0].title).toBe('Architecture Overview');
    expect(pages[0].summary).toBe('Updated the overview section');
    expect(pages[0].body).toBe('# Architecture\n\nNew content for the page.');
  });

  it('returns empty array when directory does not exist', async () => {
    expect(await readPageFiles(testDir)).toEqual([]);
  });

  it('ignores non-.md files', async () => {
    const pagesDir = join(testDir, '.cw', 'output', 'pages');
    mkdirSync(pagesDir, { recursive: true });
    writeFileSync(join(pagesDir, 'readme.txt'), 'not a page', 'utf-8');
    writeFileSync(join(pagesDir, 'page1.md'), '---\ntitle: Page 1\n---\nContent.\n', 'utf-8');

    const pages = await readPageFiles(testDir);
    expect(pages).toHaveLength(1);
  });
});

describe('writeErrandManifest', () => {
  let errandTestDir: string;

  beforeEach(() => {
    errandTestDir = join(tmpdir(), `cw-errand-test-${randomUUID()}`);
    mkdirSync(errandTestDir, { recursive: true });
  });

  afterAll(() => {
    // no-op: beforeEach creates dirs, afterEach in outer scope cleans up
  });

  it('writes manifest.json with correct shape', async () => {
    await writeErrandManifest({
      agentWorkdir: errandTestDir,
      errandId: 'errand-abc',
      description: 'fix typo',
      branch: 'cw/errand/fix-typo-errandabc',
      projectName: 'my-project',
      agentId: 'agent-1',
      agentName: 'swift-owl',
    });

    const manifestPath = join(errandTestDir, '.cw', 'input', 'manifest.json');
    expect(existsSync(manifestPath)).toBe(true);
    const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
    expect(manifest).toEqual({
      errandId: 'errand-abc',
      agentId: 'agent-1',
      agentName: 'swift-owl',
      mode: 'errand',
    });
    expect('files' in manifest).toBe(false);
    expect('contextFiles' in manifest).toBe(false);
  });

  it('writes errand.md with correct YAML frontmatter', async () => {
    await writeErrandManifest({
      agentWorkdir: errandTestDir,
      errandId: 'errand-abc',
      description: 'fix typo',
      branch: 'cw/errand/fix-typo-errandabc',
      projectName: 'my-project',
      agentId: 'agent-1',
      agentName: 'swift-owl',
    });

    const errandMdPath = join(errandTestDir, '.cw', 'input', 'errand.md');
    expect(existsSync(errandMdPath)).toBe(true);
    const content = readFileSync(errandMdPath, 'utf-8');
    expect(content).toContain('id: errand-abc');
    expect(content).toContain('description: fix typo');
    expect(content).toContain('branch: cw/errand/fix-typo-errandabc');
    expect(content).toContain('project: my-project');
  });

  it('writes expected-pwd.txt with agentWorkdir path', async () => {
    await writeErrandManifest({
      agentWorkdir: errandTestDir,
      errandId: 'errand-abc',
      description: 'fix typo',
      branch: 'cw/errand/fix-typo-errandabc',
      projectName: 'my-project',
      agentId: 'agent-1',
      agentName: 'swift-owl',
    });

    const pwdPath = join(errandTestDir, '.cw', 'expected-pwd.txt');
    expect(existsSync(pwdPath)).toBe(true);
    const content = readFileSync(pwdPath, 'utf-8').trim();
    expect(content).toBe(errandTestDir);
  });

  it('creates input directory if it does not exist', async () => {
    const freshDir = join(tmpdir(), `cw-errand-fresh-${randomUUID()}`);
    mkdirSync(freshDir, { recursive: true });

    await writeErrandManifest({
      agentWorkdir: freshDir,
      errandId: 'errand-xyz',
      description: 'add feature',
      branch: 'cw/errand/add-feature-errandxyz',
      projectName: 'other-project',
      agentId: 'agent-2',
      agentName: 'brave-eagle',
    });

    expect(existsSync(join(freshDir, '.cw', 'input', 'manifest.json'))).toBe(true);
    expect(existsSync(join(freshDir, '.cw', 'input', 'errand.md'))).toBe(true);
    expect(existsSync(join(freshDir, '.cw', 'expected-pwd.txt'))).toBe(true);

    rmSync(freshDir, { recursive: true, force: true });
  });
});

describe('buildErrandPrompt', () => {
  it('includes the description in the output', () => {
    const result = buildErrandPrompt('fix typo in README');
    expect(result).toContain('fix typo in README');
  });

  it('includes signal.json instruction', () => {
    const result = buildErrandPrompt('some change');
    expect(result).toContain('signal.json');
    expect(result).toContain('"status": "done"');
  });

  it('includes error signal format', () => {
    const result = buildErrandPrompt('some change');
    expect(result).toContain('"status": "error"');
  });

  it('includes instructions for asking questions', () => {
    const result = buildErrandPrompt('some change');
    expect(result).toMatch(/ask|question/i);
    expect(result).toMatch(/chat|message|reply/i);
  });

  it('includes questions signal format for session-ending questions', () => {
    const result = buildErrandPrompt('some change');
    expect(result).toContain('"status": "questions"');
    expect(result).toContain('"questions"');
  });

  it('explains session ends and resumes with user answers', () => {
    const result = buildErrandPrompt('some change');
    expect(result).toMatch(/resume|end.*session|session.*end/i);
  });

  it('does not present inline asking as an alternative that bypasses signal.json', () => {
    const result = buildErrandPrompt('some change');
    // "session stays open" implied agents can skip signal.json — all exits must write it
    expect(result).not.toMatch(/session stays open/i);
    expect(result).not.toMatch(/Option A/i);
  });

  it('requires signal.json for all question-asking paths', () => {
    const result = buildErrandPrompt('some change');
    // questions status must be the mechanism for all user-input requests
    expect(result).toContain('"status": "questions"');
    // must not describe a path that skips signal.json
    expect(result).not.toMatch(/session stays open/i);
  });
});
