'use strict';

/**
 * Integration tests for sub-agent support (Phase 3).
 *
 * Tests:
 *  3.1 Sub-agent detection via spawnedBy in sessions.json
 *  3.2 Nested status rendering in status-formatter
 *  3.3 Cascade completion: parent waits for all children
 *  3.4 Status watcher handles child session transcript
 */

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 { SessionMonitor } = require('../../src/session-monitor');
const { StatusWatcher } = require('../../src/status-watcher');
const { format } = require('../../src/status-formatter');

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

function writeSessionsJson(dir, agentId, sessions) {
  var agentDir = path.join(dir, agentId, 'sessions');
  fs.mkdirSync(agentDir, { recursive: true });
  fs.writeFileSync(path.join(agentDir, 'sessions.json'), JSON.stringify(sessions, null, 2));
  // Create stub transcript files so SessionMonitor doesn't skip them as missing
  for (var key in sessions) {
    var entry = sessions[key]; // eslint-disable-line security/detect-object-injection
    var sessionId = entry.sessionId || entry.uuid;
    if (sessionId) {
      var transcriptPath = path.join(agentDir, sessionId + '.jsonl');
      if (!fs.existsSync(transcriptPath)) {
        fs.writeFileSync(transcriptPath, '');
      }
    }
  }
}

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

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

describe('Sub-Agent Support (Phase 3)', () => {
  let tmpDir;

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

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

  describe('3.1 Sub-agent detection via spawnedBy', () => {
    it('detects sub-agent session with spawnedBy field', async () => {
      const parentKey = 'agent:main:mattermost:channel:chan1:thread:root1';
      const childKey = 'agent:main:subagent:uuid-child-123';
      const parentId = 'parent-uuid';
      const childId = 'child-uuid';

      writeSessionsJson(tmpDir, 'main', {
        [parentKey]: {
          sessionId: parentId,
          spawnedBy: null,
          spawnDepth: 0,
          channel: 'mattermost',
        },
        [childKey]: {
          sessionId: childId,
          spawnedBy: parentKey,
          spawnDepth: 1,
          label: 'proj035-planner',
          channel: 'mattermost',
        },
      });

      const monitor = new SessionMonitor({ transcriptDir: tmpDir, pollMs: 50 });

      const added = [];
      monitor.on('session-added', (info) => added.push(info));
      monitor.start();

      await sleep(200);
      monitor.stop();

      // Both sessions should be detected
      assert.equal(added.length, 2);

      const child = added.find((s) => s.sessionKey === childKey);
      assert.ok(child);
      assert.equal(child.spawnedBy, parentKey);
      assert.equal(child.spawnDepth, 1);
      assert.equal(child.agentId, 'proj035-planner');
    });

    it('sub-agent inherits parent channel ID', async () => {
      const parentKey = 'agent:main:mattermost:channel:mychan:thread:myroot';
      const childKey = 'agent:main:subagent:child-uuid';

      writeSessionsJson(tmpDir, 'main', {
        [parentKey]: {
          sessionId: 'p-uuid',
          spawnedBy: null,
          channel: 'mattermost',
        },
        [childKey]: {
          sessionId: 'c-uuid',
          spawnedBy: parentKey,
          spawnDepth: 1,
          channel: 'mattermost',
        },
      });

      const monitor = new SessionMonitor({ transcriptDir: tmpDir, pollMs: 50 });

      const added = [];
      monitor.on('session-added', (info) => added.push(info));
      monitor.start();

      await sleep(200);
      monitor.stop();

      const child = added.find((s) => s.sessionKey === childKey);
      assert.ok(child);
      // Child session key has the parent's channel embedded in spawnedBy key
      assert.equal(child.spawnedBy, parentKey);
    });
  });

  describe('3.2 Nested status rendering', () => {
    it('renders parent with nested child status', () => {
      const parentState = {
        sessionKey: 'agent:main:mattermost:channel:c1:thread:t1',
        status: 'active',
        startTime: Date.now() - 60000,
        lines: ['Starting implementation...', '  exec: ls [OK]'],
        agentId: 'main',
        depth: 0,
        tokenCount: 5000,
        children: [
          {
            sessionKey: 'agent:main:subagent:planner-uuid',
            status: 'done',
            startTime: Date.now() - 30000,
            lines: ['Reading protocol...', '  Read: PLAN.md [OK]'],
            agentId: 'proj035-planner',
            depth: 1,
            tokenCount: 2000,
            children: [],
          },
        ],
      };

      const result = format(parentState);
      const lines = result.split('\n');

      // Parent header at depth 0 — wrapped in blockquote ("> ")
      assert.ok(lines[0].includes('[ACTIVE]'));
      assert.ok(lines[0].includes('main'));

      // Child header at depth 1 (indented with "> " + 2 spaces)
      const childHeaderLine = lines.find((l) => l.includes('proj035-planner'));
      assert.ok(childHeaderLine);
      // Child lines appear as ">   ..." (blockquote + 2-space indent)
      assert.ok(/^> {2}/.test(childHeaderLine));

      // Child should have [DONE] line
      assert.ok(result.includes('[DONE]'));

      // Parent should not have [DONE] footer (still active)
      // The top-level [DONE] is from child, wrapped in blockquote
      const parentDoneLines = lines.filter((l) => /^\[DONE\]/.test(l));
      assert.equal(parentDoneLines.length, 0);
    });

    it('renders multiple nested children', () => {
      const child1 = {
        sessionKey: 'agent:main:subagent:child1',
        status: 'done',
        startTime: Date.now() - 20000,
        lines: ['Child 1 done'],
        agentId: 'child-agent-1',
        depth: 1,
        tokenCount: 1000,
        children: [],
      };
      const child2 = {
        sessionKey: 'agent:main:subagent:child2',
        status: 'active',
        startTime: Date.now() - 10000,
        lines: ['Child 2 working...'],
        agentId: 'child-agent-2',
        depth: 1,
        tokenCount: 500,
        children: [],
      };
      const parent = {
        sessionKey: 'agent:main:mattermost:channel:c1:thread:t1',
        status: 'active',
        startTime: Date.now() - 30000,
        lines: ['Orchestrating...'],
        agentId: 'main',
        depth: 0,
        tokenCount: 3000,
        children: [child1, child2],
      };

      const result = format(parent);
      assert.ok(result.includes('child-agent-1'));
      assert.ok(result.includes('child-agent-2'));
    });

    it('indents deeply nested children', () => {
      const grandchild = {
        sessionKey: 'agent:main:subagent:grandchild',
        status: 'active',
        startTime: Date.now() - 5000,
        lines: ['Deep work...'],
        agentId: 'grandchild-agent',
        depth: 2,
        tokenCount: 100,
        children: [],
      };
      const child = {
        sessionKey: 'agent:main:subagent:child',
        status: 'active',
        startTime: Date.now() - 15000,
        lines: ['Spawning grandchild...'],
        agentId: 'child-agent',
        depth: 1,
        tokenCount: 500,
        children: [grandchild],
      };
      const parent = {
        sessionKey: 'agent:main:mattermost:channel:c1:thread:t1',
        status: 'active',
        startTime: Date.now() - 30000,
        lines: ['Top level'],
        agentId: 'main',
        depth: 0,
        tokenCount: 2000,
        children: [child],
      };

      const result = format(parent);
      const lines = result.split('\n');

      const grandchildLine = lines.find((l) => l.includes('grandchild-agent'));
      assert.ok(grandchildLine);
      // Grandchild at depth 2 should have 4 spaces of indent after blockquote prefix ("> ")
      assert.ok(/^> {4}/.test(grandchildLine));
    });
  });

  describe('3.3 Cascade completion', () => {
    it('status-watcher tracks pending tool calls across child sessions', () => {
      const parentFile = path.join(tmpDir, 'parent.jsonl');
      const childFile = path.join(tmpDir, 'child.jsonl');

      fs.writeFileSync(parentFile, '');
      fs.writeFileSync(childFile, '');

      appendLine(parentFile, { type: 'assistant', text: 'Starting...' });
      appendLine(childFile, { type: 'tool_call', name: 'exec', id: '1' });

      const watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 600 });
      try {
        watcher.addSession('parent:key', parentFile, { depth: 0, lastOffset: 0 });
        watcher.addSession('child:key', childFile, { depth: 1, lastOffset: 0 });

        const parentState = watcher.getSessionState('parent:key');
        const childState = watcher.getSessionState('child:key');

        assert.ok(parentState);
        assert.ok(childState);
        assert.equal(childState.pendingToolCalls, 1);
        assert.equal(parentState.pendingToolCalls, 0);
      } finally {
        watcher.stop();
      }
    });

    it('child session idle does not affect parent tracking', async () => {
      const parentFile = path.join(tmpDir, 'parent2.jsonl');
      const childFile = path.join(tmpDir, 'child2.jsonl');

      fs.writeFileSync(parentFile, '');
      fs.writeFileSync(childFile, '');

      appendLine(parentFile, { type: 'assistant', text: 'Orchestrating' });
      appendLine(childFile, { type: 'assistant', text: 'Child working' });

      const watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 0.2 });
      const idleEvents = [];
      watcher.on('session-idle', (key) => idleEvents.push(key));

      try {
        watcher.addSession('parent2:key', parentFile, { depth: 0, lastOffset: 0 });
        watcher.addSession('child2:key', childFile, { depth: 1, lastOffset: 0 });

        await sleep(600);

        // Both should eventually idle (since both have no pending tool calls)
        assert.ok(idleEvents.includes('child2:key') || idleEvents.includes('parent2:key'));
      } finally {
        watcher.stop();
      }
    });
  });

  describe('3.4 Sub-agent JSONL parsing', () => {
    it('correctly parses sub-agent transcript with usage events', () => {
      const file = path.join(tmpDir, 'subagent.jsonl');
      fs.writeFileSync(file, '');
      appendLine(file, { type: 'assistant', text: 'Reading PLAN.md...' });
      appendLine(file, { type: 'tool_call', name: 'Read', id: '1' });
      appendLine(file, { type: 'tool_result', name: 'Read', id: '1' });
      appendLine(file, { type: 'usage', input_tokens: 2000, output_tokens: 800 });
      appendLine(file, { type: 'assistant', text: 'Plan ready.' });

      const watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 600 });
      try {
        watcher.addSession('subagent:key', file, { agentId: 'planner', depth: 1, lastOffset: 0 });
        const state = watcher.getSessionState('subagent:key');

        assert.ok(state.lines.some((l) => l.includes('Reading PLAN.md')));
        assert.ok(state.lines.some((l) => l.includes('Read') && l.includes('[OK]')));
        assert.ok(state.lines.some((l) => l.includes('Plan ready')));
        assert.equal(state.tokenCount, 2800);
        assert.equal(state.pendingToolCalls, 0);
      } finally {
        watcher.stop();
      }
    });

    it('handles error tool result', () => {
      const file = path.join(tmpDir, 'err.jsonl');
      fs.writeFileSync(file, '');
      appendLine(file, { type: 'tool_call', name: 'exec', id: '1' });
      appendLine(file, { type: 'tool_result', name: 'exec', id: '1', error: true });

      const watcher = new StatusWatcher({ transcriptDir: tmpDir, idleTimeoutS: 600 });
      try {
        watcher.addSession('err:key', file, { lastOffset: 0 });
        const state = watcher.getSessionState('err:key');

        const execLine = state.lines.find((l) => l.includes('exec'));
        assert.ok(execLine);
        assert.ok(execLine.includes('[ERR]'));
        assert.equal(state.pendingToolCalls, 0);
      } finally {
        watcher.stop();
      }
    });
  });
});
