'use strict';

/**
 * Integration tests for status-watcher.js
 * Tests JSONL file watching and event emission.
 */

const { describe, it, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const os = require('os');

const { StatusWatcher } = require('../../src/status-watcher');

function createTmpDir() {
  return fs.mkdtempSync(path.join(os.tmpdir(), 'sw-test-'));
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function appendLine(file, obj) {
  fs.appendFileSync(file, JSON.stringify(obj) + '\n');
}

describe('StatusWatcher', () => {
  let tmpDir;
  let watcher;

  beforeEach(() => {
    tmpDir = createTmpDir();
  });

  afterEach(() => {
    if (watcher) {
      watcher.stop();
      watcher = null;
    }
    try {
      fs.rmSync(tmpDir, { recursive: true, force: true });
    } catch (_e) {
      /* ignore */
    }
  });

  describe('session management', () => {
    it('addSession() registers a session', () => {
      watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 600 });
      const file = path.join(tmpDir, 'test.jsonl');
      fs.writeFileSync(file, '');

      watcher.addSession('test:key', file);
      assert.ok(watcher.sessions.has('test:key'));
      assert.ok(watcher.fileToSession.has(file));
    });

    it('removeSession() cleans up', () => {
      watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 600 });
      const file = path.join(tmpDir, 'test.jsonl');
      fs.writeFileSync(file, '');

      watcher.addSession('test:key', file);
      watcher.removeSession('test:key');
      assert.ok(!watcher.sessions.has('test:key'));
      assert.ok(!watcher.fileToSession.has(file));
    });

    it('getSessionState() returns state', () => {
      watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 600 });
      const file = path.join(tmpDir, 'test.jsonl');
      fs.writeFileSync(file, '');
      watcher.addSession('test:key', file);
      const state = watcher.getSessionState('test:key');
      assert.ok(state);
      assert.equal(state.sessionKey, 'test:key');
    });
  });

  describe('JSONL parsing', () => {
    it('reads existing content on addSession', () => {
      const file = path.join(tmpDir, 'test.jsonl');
      fs.writeFileSync(file, '');
      appendLine(file, { type: 'assistant', text: 'Hello world' });
      appendLine(file, { type: 'tool_call', name: 'exec', id: '1' });

      watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 600 });
      // lastOffset: 0 explicitly reads from beginning (no lastOffset = skip to end)
      watcher.addSession('test:key', file, { lastOffset: 0 });

      const state = watcher.getSessionState('test:key');
      assert.ok(state.lines.length > 0);
      assert.ok(state.lines.some((l) => l.includes('exec')));
    });

    it('emits session-update when file changes', async () => {
      const file = path.join(tmpDir, 'test.jsonl');
      fs.writeFileSync(file, '');

      watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 600 });
      watcher.start();
      watcher.addSession('test:key', file);

      const updates = [];
      watcher.on('session-update', (key, state) => updates.push({ key, state }));

      await sleep(100);
      appendLine(file, { type: 'assistant', text: 'Starting task...' });
      await sleep(500);

      assert.ok(updates.length > 0);
      assert.equal(updates[0].key, 'test:key');
    });

    it('parses tool_call and tool_result pairs', () => {
      const file = path.join(tmpDir, 'test.jsonl');
      fs.writeFileSync(file, '');
      appendLine(file, { type: 'tool_call', name: 'exec', id: '1' });
      appendLine(file, { type: 'tool_result', name: 'exec', id: '1' });

      watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 600 });
      watcher.addSession('test:key', file, { lastOffset: 0 });

      const state = watcher.getSessionState('test:key');
      assert.equal(state.pendingToolCalls, 0);
      // Line should show [OK]
      const execLine = state.lines.find((l) => l.includes('exec'));
      assert.ok(execLine);
      assert.ok(execLine.includes('[OK]'));
    });

    it('tracks pendingToolCalls correctly', () => {
      const file = path.join(tmpDir, 'test.jsonl');
      fs.writeFileSync(file, '');
      appendLine(file, { type: 'tool_call', name: 'exec', id: '1' });
      appendLine(file, { type: 'tool_call', name: 'Read', id: '2' });
      appendLine(file, { type: 'tool_result', name: 'exec', id: '1' });

      watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 600 });
      watcher.addSession('test:key', file, { lastOffset: 0 });

      const state = watcher.getSessionState('test:key');
      assert.equal(state.pendingToolCalls, 1); // Read still pending
    });

    it('tracks token usage', () => {
      const file = path.join(tmpDir, 'test.jsonl');
      fs.writeFileSync(file, '');
      appendLine(file, { type: 'usage', input_tokens: 1000, output_tokens: 500 });

      watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 600 });
      watcher.addSession('test:key', file, { lastOffset: 0 });

      const state = watcher.getSessionState('test:key');
      assert.equal(state.tokenCount, 1500);
    });

    it('detects file truncation (compaction)', () => {
      const file = path.join(tmpDir, 'test.jsonl');

      // Write some content
      fs.writeFileSync(
        file,
        JSON.stringify({ type: 'assistant', text: 'Hello' }) +
          '\n' +
          JSON.stringify({ type: 'tool_call', name: 'exec', id: '1' }) +
          '\n',
      );

      watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 600 });
      watcher.addSession('test:key', file);

      const state = watcher.getSessionState('test:key');
      const originalOffset = state.lastOffset;
      assert.ok(originalOffset > 0);

      // Truncate the file (simulate compaction)
      fs.writeFileSync(file, JSON.stringify({ type: 'assistant', text: 'Resumed' }) + '\n');

      // Force a re-read
      watcher._readFile('test:key', state);

      // Offset should have reset
      assert.ok(
        state.lastOffset < originalOffset ||
          state.lines.includes('[session compacted - continuing]'),
      );
    });

    it('handles malformed JSONL lines gracefully', () => {
      const file = path.join(tmpDir, 'test.jsonl');
      fs.writeFileSync(file, 'not valid json\n{"type":"assistant","text":"ok"}\n');

      watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 600 });

      // Should not throw
      assert.doesNotThrow(() => {
        watcher.addSession('test:key', file, { lastOffset: 0 });
      });

      const state = watcher.getSessionState('test:key');
      assert.ok(state);
      assert.ok(state.lines.some((l) => l.includes('ok')));
    });
  });

  describe('idle detection', () => {
    it('emits session-idle after timeout with no activity', async () => {
      const file = path.join(tmpDir, 'test.jsonl');
      fs.writeFileSync(file, '');
      appendLine(file, { type: 'assistant', text: 'Starting...' });

      watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 0.2 }); // 200ms
      // lastOffset: 0 so the pre-written content is read and idle timer is scheduled
      watcher.addSession('test:key', file, { lastOffset: 0 });

      const idle = [];
      watcher.on('session-idle', (key) => idle.push(key));

      await sleep(600);
      assert.ok(idle.length > 0);
      assert.equal(idle[0], 'test:key');
    });

    it('resets idle timer on new activity', async () => {
      const file = path.join(tmpDir, 'test.jsonl');
      fs.writeFileSync(file, '');

      watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 0.3 }); // 300ms
      watcher.start();
      watcher.addSession('test:key', file);

      const idle = [];
      watcher.on('session-idle', (key) => idle.push(key));

      // Write activity at 150ms (before 300ms timeout)
      await sleep(150);
      appendLine(file, { type: 'assistant', text: 'Still working...' });

      // At 350ms: idle timer was reset, so no idle yet
      await sleep(200);
      assert.equal(idle.length, 0);

      // At 600ms: idle timer fires (150+300=450ms > 200+300=500ms... need to wait full 300ms)
      await sleep(400);
      assert.equal(idle.length, 1);
    });
  });

  describe('offset recovery', () => {
    it('resumes from saved offset', () => {
      const file = path.join(tmpDir, 'test.jsonl');
      const line1 = JSON.stringify({ type: 'assistant', text: 'First' }) + '\n';
      const line2 = JSON.stringify({ type: 'assistant', text: 'Second' }) + '\n';
      fs.writeFileSync(file, line1 + line2);

      watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 600 });

      // Start with offset at start of line2 (after line1)
      watcher.addSession('test:key', file, { lastOffset: line1.length });

      const state = watcher.getSessionState('test:key');
      // Should only have parsed line2
      assert.ok(state.lines.some((l) => l.includes('Second')));
      assert.ok(!state.lines.some((l) => l.includes('First')));
    });
  });
});
