/**
 * Integration test to reproduce and fix the crash marking race condition.
 *
 * This test simulates the exact scenario where agents complete successfully
 * but get marked as crashed due to timing issues in the output handler.
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { writeFile, mkdir, rm } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { randomBytes } from 'node:crypto';
import { OutputHandler } from '../../agent/output-handler.js';
import type { AgentRepository } from '../../db/repositories/agent-repository.js';

interface TestAgent {
  id: string;
  name: string;
  status: 'idle' | 'running' | 'waiting_for_input' | 'stopped' | 'crashed';
  mode: 'execute' | 'discuss' | 'plan' | 'detail' | 'refine';
  taskId: string | null;
  sessionId: string | null;
  worktreeId: string;
  createdAt: Date;
  updatedAt: Date;
  provider: string;
  accountId: string | null;
  pid: number | null;
  outputFilePath: string | null;
  result: string | null;
  pendingQuestions: string | null;
  initiativeId: string | null;
  userDismissedAt: Date | null;
  exitCode: number | null;
}

describe('Crash marking race condition', () => {
  let outputHandler: OutputHandler;
  let testAgent: TestAgent;
  let testDir: string;
  let mockRepo: AgentRepository;

  // Track all repository calls
  let updateCalls: Array<{ id: string; data: any }> = [];
  let finalAgentStatus: string | null = null;

  beforeEach(async () => {
    updateCalls = [];
    finalAgentStatus = null;

    // Create test directory structure
    testDir = join(tmpdir(), `crash-test-${randomBytes(8).toString('hex')}`);
    const outputDir = join(testDir, '.cw/output');
    await mkdir(outputDir, { recursive: true });

    // Create test agent
    testAgent = {
      id: 'test-agent-id',
      name: 'test-agent',
      status: 'running',
      mode: 'refine',
      taskId: 'task-1',
      sessionId: 'session-1',
      worktreeId: 'worktree-1',
      createdAt: new Date(),
      updatedAt: new Date(),
      provider: 'claude',
      accountId: null,
      pid: 12345,
      outputFilePath: join(testDir, 'output.jsonl'),
      result: null,
      pendingQuestions: null,
      initiativeId: 'init-1',
      userDismissedAt: null,
      exitCode: null
    };

    // Mock repository that tracks all update calls
    mockRepo = {
      async findById(id: string) {
        return id === testAgent.id ? { ...testAgent } : null;
      },
      async update(id: string, data: any) {
        updateCalls.push({ id, data });
        if (data.status) {
          finalAgentStatus = data.status;
          testAgent.status = data.status;
        }
        return { ...testAgent, ...data };
      },
      async create() { throw new Error('Not implemented'); },
      async findAll() { throw new Error('Not implemented'); },
      async findByStatus() { throw new Error('Not implemented'); },
      async findByTaskId() { throw new Error('Not implemented'); },
      async findByName() { throw new Error('Not implemented'); },
      async findBySessionId() { throw new Error('Not implemented'); },
      async delete() { throw new Error('Not implemented'); }
    };

    outputHandler = new OutputHandler(mockRepo);
  });

  afterEach(async () => {
    try {
      await rm(testDir, { recursive: true });
    } catch {
      // Ignore cleanup errors
    }
  });

  it('should NOT mark agent as crashed when signal.json indicates completion', async () => {
    // SETUP: Create a valid completion signal that should prevent crash marking
    const signalPath = join(testDir, '.cw/output/signal.json');
    const signalContent = {
      status: 'questions',
      questions: [
        { id: 'q1', question: 'Test question?' }
      ]
    };
    await writeFile(signalPath, JSON.stringify(signalContent, null, 2));

    // SETUP: Create empty output file to simulate "no new output detected" scenario
    const outputFilePath = join(testDir, 'output.jsonl');
    await writeFile(outputFilePath, ''); // Empty file simulates the race condition

    // Mock active agent with output file path
    const mockActive = {
      outputFilePath,
      streamSessionId: 'session-1'
    };

    // Mock getAgentWorkdir function — receives worktreeId, not agentId
    const getAgentWorkdir = (worktreeId: string) => {
      expect(worktreeId).toBe(testAgent.worktreeId);
      return testDir;
    };

    // EXECUTE: Call handleCompletion which should trigger the race condition scenario
    // This simulates: no stream text + no new file content + valid signal.json
    await (outputHandler as any).handleCompletion(
      testAgent.id,
      mockActive,
      getAgentWorkdir
    );

    // VERIFY: Agent should NOT be marked as crashed
    console.log('Update calls:', updateCalls);
    console.log('Final agent status:', finalAgentStatus);

    expect(updateCalls.length).toBeGreaterThan(0);
    expect(finalAgentStatus).not.toBe('crashed');

    // Should be marked with the appropriate completion status
    expect(['idle', 'waiting_for_input', 'stopped']).toContain(finalAgentStatus);
  });

  it('should mark agent as crashed when no completion signal exists', async () => {
    // SETUP: No signal.json file exists - agent should be marked as crashed
    const outputFilePath = join(testDir, 'output.jsonl');
    await writeFile(outputFilePath, ''); // Empty file

    const mockActive = {
      outputFilePath,
      streamSessionId: 'session-1'
    };

    const getAgentWorkdir = (agentId: string) => testDir;

    // EXECUTE: This should mark agent as crashed since no completion signal exists
    await (outputHandler as any).handleCompletion(
      testAgent.id,
      mockActive,
      getAgentWorkdir
    );

    // VERIFY: Agent SHOULD be marked as crashed
    expect(finalAgentStatus).toBe('crashed');
  });

  it('should handle the exact slim-wildebeest scenario', async () => {
    // SETUP: Reproduce the exact conditions that slim-wildebeest had
    const signalPath = join(testDir, '.cw/output/signal.json');
    const exactSignalContent = {
      "status": "questions",
      "questions": [
        {
          "id": "q1",
          "question": "What UI framework/styling system is the admin UI currently using that needs to be replaced?"
        },
        {
          "id": "q2",
          "question": "What specific problems with the current admin UI are we solving? (e.g., poor developer experience, design inconsistency, performance issues, lack of accessibility)"
        }
      ]
    };
    await writeFile(signalPath, JSON.stringify(exactSignalContent, null, 2));

    // Create SUMMARY.md like slim-wildebeest had
    const summaryPath = join(testDir, '.cw/output/SUMMARY.md');
    const summaryContent = `---
files_modified: []
---
Initiative page is essentially empty — lacks context, scope, goals, and technical approach. Requested clarification on current state, problems being solved, scope boundaries, and success criteria before proposing meaningful improvements.`;
    await writeFile(summaryPath, summaryContent);

    // Simulate the output file scenario
    const outputFilePath = join(testDir, 'output.jsonl');
    await writeFile(outputFilePath, 'some initial content\n'); // Some content but no new lines

    const mockActive = {
      outputFilePath,
      streamSessionId: 'session-1'
    };

    const getAgentWorkdir = (agentId: string) => testDir;

    // EXECUTE: This is the exact scenario that caused slim-wildebeest to be marked as crashed
    await (outputHandler as any).handleCompletion(
      testAgent.id,
      mockActive,
      getAgentWorkdir
    );

    // VERIFY: This should NOT be marked as crashed
    console.log('slim-wildebeest scenario - Final status:', finalAgentStatus);
    console.log('slim-wildebeest scenario - Update calls:', updateCalls);

    expect(finalAgentStatus).not.toBe('crashed');
    expect(['idle', 'waiting_for_input', 'stopped']).toContain(finalAgentStatus);
  });

});