/**
 * ProcessManager Unit Tests
 *
 * Tests for ProcessManager class focusing on working directory handling,
 * command building, and spawn validation.
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { ProcessManager } from './process-manager.js';
import type { ProjectRepository } from '../db/repositories/project-repository.js';

// Mock child_process.spawn
vi.mock('node:child_process', () => ({
  spawn: vi.fn(),
}));

// Mock fs operations
vi.mock('node:fs', () => ({
  openSync: vi.fn((path) => {
    // Return different fd numbers for stdout and stderr
    if (path.includes('output.jsonl')) return 99;
    if (path.includes('stderr.log')) return 100;
    return 101;
  }),
  closeSync: vi.fn(),
  existsSync: vi.fn(),
}));

vi.mock('node:fs/promises', () => ({
  mkdir: vi.fn().mockResolvedValue(undefined),
  writeFile: vi.fn().mockResolvedValue(undefined),
}));

// Mock FileTailer
vi.mock('./file-tailer.js', () => ({
  FileTailer: class MockFileTailer {
    start = vi.fn().mockResolvedValue(undefined);
    stop = vi.fn().mockResolvedValue(undefined);
  },
}));

// Mock SimpleGitWorktreeManager
const mockCreate = vi.fn();
vi.mock('../git/manager.js', () => ({
  SimpleGitWorktreeManager: class MockWorktreeManager {
    create = mockCreate;
  },
}));

// Mock project clones
vi.mock('../git/project-clones.js', () => ({
  ensureProjectClone: vi.fn().mockResolvedValue('/mock/clone/path'),
  getProjectCloneDir: vi.fn().mockReturnValue('/mock/clone/path'),
}));

// Mock providers
vi.mock('./providers/parsers/index.js', () => ({
  getStreamParser: vi.fn().mockReturnValue({ parse: vi.fn() }),
}));

import { spawn } from 'node:child_process';
import { existsSync, openSync, closeSync } from 'node:fs';
import { mkdir, writeFile } from 'node:fs/promises';
import { ensureProjectClone } from '../git/project-clones.js';

const mockSpawn = vi.mocked(spawn);
const mockExistsSync = vi.mocked(existsSync);
const mockMkdir = vi.mocked(mkdir);
const mockWriteFile = vi.mocked(writeFile);
const mockOpenSync = vi.mocked(openSync);
const mockCloseSync = vi.mocked(closeSync);

describe('ProcessManager', () => {
  let processManager: ProcessManager;
  let mockProjectRepository: ProjectRepository;

  const workspaceRoot = '/test/workspace';

  beforeEach(() => {
    vi.clearAllMocks();

    // Mock child process
    const mockChild = {
      pid: 12345,
      unref: vi.fn(),
      on: vi.fn(),
      kill: vi.fn(),
    };
    mockSpawn.mockReturnValue(mockChild as any);

    // Mock project repository
    mockProjectRepository = {
      findProjectsByInitiativeId: vi.fn().mockResolvedValue([]),
      create: vi.fn(),
      findAll: vi.fn(),
      findById: vi.fn(),
      findByName: vi.fn(),
      update: vi.fn(),
      delete: vi.fn(),
      setInitiativeProjects: vi.fn(),
      addProjectToInitiative: vi.fn(),
      removeProjectFromInitiative: vi.fn(),
    };

    processManager = new ProcessManager(workspaceRoot, mockProjectRepository);
  });

  afterEach(() => {
    vi.resetAllMocks();
  });

  describe('getAgentWorkdir', () => {
    it('returns correct agent workdir path', () => {
      const alias = 'test-agent';
      const expected = '/test/workspace/agent-workdirs/test-agent';

      const result = processManager.getAgentWorkdir(alias);

      expect(result).toBe(expected);
    });
  });

  describe('createProjectWorktrees', () => {
    beforeEach(() => {
      // Mock the global worktree create function
      mockCreate.mockResolvedValue({
        id: 'project1',
        path: '/test/workspace/agent-workdirs/test-agent/project1',
        branch: 'agent/test-agent',
        isMainWorktree: false,
      });

      // Mock project repository
      vi.mocked(mockProjectRepository.findProjectsByInitiativeId).mockResolvedValue([
        { id: '1', name: 'project1', url: 'https://github.com/user/project1.git', defaultBranch: 'main', lastFetchedAt: null, createdAt: new Date(), updatedAt: new Date() }
      ]);

      // Mock existsSync to return true for worktree paths
      mockExistsSync.mockImplementation((path) => {
        return path.toString().includes('/agent-workdirs/');
      });
    });

    it('creates worktrees for initiative projects', async () => {
      const alias = 'test-agent';
      const initiativeId = 'init-123';

      const result = await processManager.createProjectWorktrees(alias, initiativeId);

      expect(result).toBe('/test/workspace/agent-workdirs/test-agent');
      expect(mockProjectRepository.findProjectsByInitiativeId).toHaveBeenCalledWith('init-123');
      expect(ensureProjectClone).toHaveBeenCalled();
    });

    it('throws error when worktree creation fails', async () => {
      // Mock worktree path to not exist after creation
      mockExistsSync.mockReturnValue(false);

      const alias = 'test-agent';
      const initiativeId = 'init-123';

      await expect(processManager.createProjectWorktrees(alias, initiativeId))
        .rejects.toThrow('Worktree creation failed:');
    });

});

  describe('createStandaloneWorktree', () => {
    beforeEach(() => {
      mockCreate.mockResolvedValue({
        id: 'workspace',
        path: '/test/workspace/agent-workdirs/test-agent/workspace',
        branch: 'agent/test-agent',
        isMainWorktree: false,
      });

      mockExistsSync.mockImplementation((path) => {
        return path.toString().includes('/workspace');
      });
    });

    it('creates standalone worktree', async () => {
      const alias = 'test-agent';

      const result = await processManager.createStandaloneWorktree(alias);

      expect(result).toBe('/test/workspace/agent-workdirs/test-agent/workspace');
    });

    it('throws error when standalone worktree creation fails', async () => {
      mockExistsSync.mockReturnValue(false);

      const alias = 'test-agent';

      await expect(processManager.createStandaloneWorktree(alias))
        .rejects.toThrow('Standalone worktree creation failed:');
    });
  });

  describe('spawnDetached', () => {
    beforeEach(() => {
      mockExistsSync.mockReturnValue(true); // CWD exists
    });

    it('validates cwd exists before spawn', async () => {
      const agentId = 'agent-123';
      const agentName = 'test-agent';
      const command = 'claude';
      const args = ['--help'];
      const cwd = '/test/workspace/agent-workdirs/test-agent';
      const env = { TEST_VAR: 'value' };
      const providerName = 'claude';

      await processManager.spawnDetached(agentId, agentName, command, args, cwd, env, providerName);

      expect(mockExistsSync).toHaveBeenCalledWith(cwd);
      expect(mockSpawn).toHaveBeenCalledWith(command, args, {
        cwd,
        env: expect.objectContaining(env),
        detached: true,
        stdio: ['ignore', 99, 100],
      });
    });

    it('throws error when cwd does not exist', async () => {
      mockExistsSync.mockReturnValue(false);

      const agentId = 'agent-123';
      const agentName = 'test-agent';
      const command = 'claude';
      const args = ['--help'];
      const cwd = '/nonexistent/path';
      const env = {};
      const providerName = 'claude';

      await expect(
        processManager.spawnDetached(agentId, agentName, command, args, cwd, env, providerName),
      ).rejects.toThrow('Agent working directory does not exist: /nonexistent/path');
    });

    it('passes correct cwd parameter to spawn', async () => {
      const agentId = 'agent-123';
      const agentName = 'test-agent';
      const command = 'claude';
      const args = ['--help'];
      const cwd = '/test/workspace/agent-workdirs/test-agent';
      const env = { CLAUDE_CONFIG_DIR: '/config' };
      const providerName = 'claude';

      await processManager.spawnDetached(agentId, agentName, command, args, cwd, env, providerName);

      expect(mockSpawn).toHaveBeenCalledTimes(1);
      const spawnCall = mockSpawn.mock.calls[0];
      expect(spawnCall[0]).toBe(command);
      expect(spawnCall[1]).toEqual(args);
      expect(spawnCall[2]).toEqual({
        cwd,
        env: expect.objectContaining({
          ...process.env,
          CLAUDE_CONFIG_DIR: '/config',
        }),
        detached: true,
        stdio: ['ignore', 99, 100],
      });
    });

    it('writes prompt file when provided', async () => {
      const agentId = 'agent-123';
      const agentName = 'test-agent';
      const command = 'claude';
      const args = ['--help'];
      const cwd = '/test/workspace/agent-workdirs/test-agent';
      const env = {};
      const providerName = 'claude';
      const prompt = 'Test prompt';

      await processManager.spawnDetached(agentId, agentName, command, args, cwd, env, providerName, prompt);

      expect(mockWriteFile).toHaveBeenCalledWith(
        '/test/workspace/.cw/agent-logs/test-agent/PROMPT.md',
        'Test prompt',
        'utf-8'
      );
    });
  });

  describe('buildSpawnCommand', () => {
    it('builds command with native prompt mode', () => {
      const provider = {
        name: 'claude',
        command: 'claude',
        args: ['--json-schema', 'schema.json'],
        env: {},
        promptMode: 'native' as const,
        processNames: ['claude'],
        resumeStyle: 'flag' as const,
        resumeFlag: '--resume',
        nonInteractive: {
          subcommand: 'chat',
          promptFlag: '-p',
          outputFlag: '--output-format json',
        },
      };
      const prompt = 'Test prompt';

      const result = processManager.buildSpawnCommand(provider, prompt);

      expect(result).toEqual({
        command: 'claude',
        args: ['chat', '--json-schema', 'schema.json', '-p', 'Test prompt', '--output-format', 'json'],
        env: {},
      });
    });

    it('builds command with flag prompt mode', () => {
      const provider = {
        name: 'codex',
        command: 'codex',
        args: ['--format', 'json'],
        env: {},
        promptMode: 'flag' as const,
        processNames: ['codex'],
        resumeStyle: 'subcommand' as const,
        resumeFlag: 'resume',
        nonInteractive: {
          subcommand: 'run',
          promptFlag: '--prompt',
          outputFlag: '--json',
        },
      };
      const prompt = 'Test prompt';

      const result = processManager.buildSpawnCommand(provider, prompt);

      expect(result).toEqual({
        command: 'codex',
        args: ['run', '--format', 'json', '--prompt', 'Test prompt', '--json'],
        env: {},
      });
    });
  });

  describe('buildResumeCommand', () => {
    it('builds resume command with flag style', () => {
      const provider = {
        name: 'claude',
        command: 'claude',
        args: [],
        env: {},
        promptMode: 'native' as const,
        processNames: ['claude'],
        resumeStyle: 'flag' as const,
        resumeFlag: '--resume',
        nonInteractive: {
          subcommand: 'chat',
          promptFlag: '-p',
          outputFlag: '--json',
        },
      };
      const sessionId = 'session-123';
      const prompt = 'Continue working';

      const result = processManager.buildResumeCommand(provider, sessionId, prompt);

      expect(result).toEqual({
        command: 'claude',
        args: ['--resume', 'session-123', '-p', 'Continue working', '--json'],
        env: {},
      });
    });

    it('throws error for providers without resume support', () => {
      const provider = {
        name: 'noresume',
        command: 'noresume',
        args: [],
        env: {},
        promptMode: 'native' as const,
        processNames: ['noresume'],
        resumeStyle: 'none' as const,
      };
      const sessionId = 'session-123';
      const prompt = 'Continue working';

      expect(() => {
        processManager.buildResumeCommand(provider, sessionId, prompt);
      }).toThrow("Provider 'noresume' does not support resume");
    });
  });
});