/**
 * ProcessLogWriter Tests
 *
 * Tests for the per-process log writing functionality.
 * Uses temporary directories to avoid polluting the real log directory.
 */

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { rm, readFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { LogManager } from './manager.js';
import { ProcessLogWriter } from './writer.js';
import { createEventBus } from '../events/index.js';
import type { EventBus, LogEntryEvent } from '../events/index.js';

describe('ProcessLogWriter', () => {
  let testDir: string;
  let manager: LogManager;
  let writer: ProcessLogWriter;
  const processId = 'test-process';

  beforeEach(async () => {
    // Create a unique temp directory for each test
    testDir = join(tmpdir(), `cw-writer-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
    manager = new LogManager({ baseDir: testDir });
    writer = new ProcessLogWriter(processId, manager);
  });

  afterEach(async () => {
    // Ensure writer is closed before cleanup
    try {
      await writer.close();
    } catch {
      // Ignore if already closed
    }

    // Clean up temp directory
    try {
      await rm(testDir, { recursive: true, force: true });
    } catch {
      // Ignore cleanup errors
    }
  });

  describe('getProcessId', () => {
    it('should return the process ID', () => {
      expect(writer.getProcessId()).toBe(processId);
    });
  });

  describe('open', () => {
    it('should create the process log directory', async () => {
      const processDir = manager.getProcessDir(processId);
      expect(existsSync(processDir)).toBe(false);

      await writer.open();

      expect(existsSync(processDir)).toBe(true);
    });

    it('should create stdout.log and stderr.log files', async () => {
      await writer.open();

      const stdoutPath = manager.getLogPath(processId, 'stdout');
      const stderrPath = manager.getLogPath(processId, 'stderr');

      expect(existsSync(stdoutPath)).toBe(true);
      expect(existsSync(stderrPath)).toBe(true);
    });

    it('should make streams available after open', async () => {
      expect(writer.getStdoutStream()).toBeNull();
      expect(writer.getStderrStream()).toBeNull();

      await writer.open();

      expect(writer.getStdoutStream()).not.toBeNull();
      expect(writer.getStderrStream()).not.toBeNull();
    });
  });

  describe('writeStdout', () => {
    it('should throw if writer is not open', async () => {
      await expect(writer.writeStdout('test')).rejects.toThrow(
        'Log writer not open'
      );
    });

    it('should write string data to stdout.log', async () => {
      await writer.open();

      await writer.writeStdout('Hello stdout\n');
      await writer.close();

      const content = await readFile(
        manager.getLogPath(processId, 'stdout'),
        'utf-8'
      );
      expect(content).toContain('Hello stdout');
    });

    it('should write Buffer data to stdout.log', async () => {
      await writer.open();

      await writer.writeStdout(Buffer.from('Buffer content\n'));
      await writer.close();

      const content = await readFile(
        manager.getLogPath(processId, 'stdout'),
        'utf-8'
      );
      expect(content).toContain('Buffer content');
    });

    it('should prefix lines with timestamps', async () => {
      await writer.open();

      await writer.writeStdout('Line one\n');
      await writer.close();

      const content = await readFile(
        manager.getLogPath(processId, 'stdout'),
        'utf-8'
      );
      // Timestamp format: [YYYY-MM-DD HH:mm:ss.SSS]
      expect(content).toMatch(/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\]/);
    });

    it('should handle multiple lines with individual timestamps', async () => {
      await writer.open();

      await writer.writeStdout('Line one\nLine two\nLine three\n');
      await writer.close();

      const content = await readFile(
        manager.getLogPath(processId, 'stdout'),
        'utf-8'
      );
      const lines = content.split('\n').filter((l) => l.length > 0);
      expect(lines.length).toBe(3);

      // Each line should have a timestamp
      for (const line of lines) {
        expect(line).toMatch(/^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\]/);
      }
    });
  });

  describe('writeStderr', () => {
    it('should throw if writer is not open', async () => {
      await expect(writer.writeStderr('test')).rejects.toThrow(
        'Log writer not open'
      );
    });

    it('should write string data to stderr.log', async () => {
      await writer.open();

      await writer.writeStderr('Error message\n');
      await writer.close();

      const content = await readFile(
        manager.getLogPath(processId, 'stderr'),
        'utf-8'
      );
      expect(content).toContain('Error message');
    });

    it('should write Buffer data to stderr.log', async () => {
      await writer.open();

      await writer.writeStderr(Buffer.from('Error buffer\n'));
      await writer.close();

      const content = await readFile(
        manager.getLogPath(processId, 'stderr'),
        'utf-8'
      );
      expect(content).toContain('Error buffer');
    });

    it('should prefix lines with timestamps', async () => {
      await writer.open();

      await writer.writeStderr('Error line\n');
      await writer.close();

      const content = await readFile(
        manager.getLogPath(processId, 'stderr'),
        'utf-8'
      );
      expect(content).toMatch(/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\]/);
    });
  });

  describe('close', () => {
    it('should flush and close file handles', async () => {
      await writer.open();
      await writer.writeStdout('Before close\n');

      await writer.close();

      // Streams should be null after close
      expect(writer.getStdoutStream()).toBeNull();
      expect(writer.getStderrStream()).toBeNull();

      // Data should be flushed to disk
      const content = await readFile(
        manager.getLogPath(processId, 'stdout'),
        'utf-8'
      );
      expect(content).toContain('Before close');
    });

    it('should not error if called multiple times', async () => {
      await writer.open();
      await writer.close();

      // Should not throw
      await writer.close();
    });
  });

  describe('write after close', () => {
    it('should throw when writing to stdout after close', async () => {
      await writer.open();
      await writer.close();

      await expect(writer.writeStdout('test')).rejects.toThrow(
        'Log writer not open'
      );
    });

    it('should throw when writing to stderr after close', async () => {
      await writer.open();
      await writer.close();

      await expect(writer.writeStderr('test')).rejects.toThrow(
        'Log writer not open'
      );
    });
  });

  describe('append mode', () => {
    it('should append to existing log files', async () => {
      // First write session
      await writer.open();
      await writer.writeStdout('First session\n');
      await writer.close();

      // Second write session with new writer
      const writer2 = new ProcessLogWriter(processId, manager);
      await writer2.open();
      await writer2.writeStdout('Second session\n');
      await writer2.close();

      const content = await readFile(
        manager.getLogPath(processId, 'stdout'),
        'utf-8'
      );
      expect(content).toContain('First session');
      expect(content).toContain('Second session');
    });
  });
});

describe('ProcessLogWriter with EventBus', () => {
  let testDir: string;
  let manager: LogManager;
  let eventBus: EventBus;
  let writerWithBus: ProcessLogWriter;
  const processId = 'event-test-process';

  beforeEach(async () => {
    testDir = join(tmpdir(), `cw-event-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
    manager = new LogManager({ baseDir: testDir });
    eventBus = createEventBus();
    writerWithBus = new ProcessLogWriter(processId, manager, eventBus);
  });

  afterEach(async () => {
    try {
      await writerWithBus.close();
    } catch {
      // Ignore if already closed
    }

    try {
      await rm(testDir, { recursive: true, force: true });
    } catch {
      // Ignore cleanup errors
    }
  });

  describe('event emission', () => {
    it('should emit log:entry event on writeStdout', async () => {
      const handler = vi.fn();
      eventBus.on<LogEntryEvent>('log:entry', handler);

      await writerWithBus.open();
      await writerWithBus.writeStdout('Hello stdout\n');

      expect(handler).toHaveBeenCalledOnce();

      const event = handler.mock.calls[0][0] as LogEntryEvent;
      expect(event.type).toBe('log:entry');
      expect(event.payload.processId).toBe(processId);
      expect(event.payload.stream).toBe('stdout');
      expect(event.payload.data).toBe('Hello stdout\n');
      expect(event.timestamp).toBeInstanceOf(Date);
    });

    it('should emit log:entry event on writeStderr', async () => {
      const handler = vi.fn();
      eventBus.on<LogEntryEvent>('log:entry', handler);

      await writerWithBus.open();
      await writerWithBus.writeStderr('Error message\n');

      expect(handler).toHaveBeenCalledOnce();

      const event = handler.mock.calls[0][0] as LogEntryEvent;
      expect(event.type).toBe('log:entry');
      expect(event.payload.processId).toBe(processId);
      expect(event.payload.stream).toBe('stderr');
      expect(event.payload.data).toBe('Error message\n');
    });

    it('should emit events with Buffer data converted to string', async () => {
      const handler = vi.fn();
      eventBus.on<LogEntryEvent>('log:entry', handler);

      await writerWithBus.open();
      await writerWithBus.writeStdout(Buffer.from('Buffer data\n'));

      const event = handler.mock.calls[0][0] as LogEntryEvent;
      expect(event.payload.data).toBe('Buffer data\n');
    });

    it('should emit event for each write call', async () => {
      const handler = vi.fn();
      eventBus.on<LogEntryEvent>('log:entry', handler);

      await writerWithBus.open();
      await writerWithBus.writeStdout('Line 1\n');
      await writerWithBus.writeStdout('Line 2\n');
      await writerWithBus.writeStderr('Error\n');

      expect(handler).toHaveBeenCalledTimes(3);

      // Verify each call
      expect(handler.mock.calls[0][0].payload.stream).toBe('stdout');
      expect(handler.mock.calls[0][0].payload.data).toBe('Line 1\n');
      expect(handler.mock.calls[1][0].payload.stream).toBe('stdout');
      expect(handler.mock.calls[1][0].payload.data).toBe('Line 2\n');
      expect(handler.mock.calls[2][0].payload.stream).toBe('stderr');
      expect(handler.mock.calls[2][0].payload.data).toBe('Error\n');
    });
  });

  describe('without eventBus', () => {
    it('should NOT emit events when eventBus is not provided', async () => {
      // Create a writer without eventBus
      const writerNoBus = new ProcessLogWriter(processId + '-nobus', manager);
      const handler = vi.fn();
      eventBus.on<LogEntryEvent>('log:entry', handler);

      await writerNoBus.open();
      await writerNoBus.writeStdout('Hello\n');
      await writerNoBus.writeStderr('Error\n');
      await writerNoBus.close();

      // Handler should not have been called because writerNoBus has no eventBus
      expect(handler).not.toHaveBeenCalled();
    });
  });

  describe('backwards compatibility', () => {
    it('should work with two-argument constructor', async () => {
      const writerCompat = new ProcessLogWriter(processId + '-compat', manager);

      await writerCompat.open();
      await writerCompat.writeStdout('Compat test\n');
      await writerCompat.close();

      const content = await readFile(
        manager.getLogPath(processId + '-compat', 'stdout'),
        'utf-8'
      );
      expect(content).toContain('Compat test');
    });
  });
});
