// Tests for background agent Smart Interrupt system:
// - Agent state persistence (save/load to disk)
// - Turn-level checkpointing
// - Startup recovery of interrupted agents

const fs = require('fs');
const path = require('path');
const os = require('os');

// Jest requires mock factory variables to start with "mock"
const mockPreDir = path.join(os.tmpdir(), `pre-test-bg-${process.pid}`);

jest.mock('../src/constants', () => ({
  PRE_DIR: mockPreDir,
  MODEL: 'pre-gemma4',
  MODEL_CTX: 8192,
  OLLAMA_PORT: 11434,
  MAX_TOOL_TURNS: 35,
  SESSIONS_DIR: require('path').join(mockPreDir, 'sessions'),
  MEMORY_DIR: require('path').join(mockPreDir, 'memory'),
  ARTIFACTS_DIR: require('path').join(mockPreDir, 'artifacts'),
  CONNECTIONS_FILE: require('path').join(mockPreDir, 'connections.json'),
  COMFYUI_FILE: require('path').join(mockPreDir, 'comfyui.json'),
  CRON_FILE: require('path').join(mockPreDir, 'cron.json'),
}));

jest.mock('../src/sessions', () => ({
  createSession: jest.fn(() => `test-session-${Date.now()}`),
  appendMessage: jest.fn(),
  renameSession: jest.fn(),
  ensureProject: jest.fn(),
  moveSessionToProject: jest.fn(),
  getSessionMessages: jest.fn(() => []),
}));

jest.mock('../src/tools', () => ({
  runToolLoop: jest.fn(async ({ send }) => {
    send({ type: 'token', content: 'Test result' });
    send({ type: 'done', stats: { prompt_eval_count: 100, eval_count: 50 } });
  }),
  waitForIdle: jest.fn(async () => {}),
}));

jest.mock('../src/ollama', () => ({
  healthCheck: jest.fn(async () => ({ ok: true })),
  streamChat: jest.fn(),
}));

const STATE_FILE = path.join(mockPreDir, 'agent-state.json');
const CHECKPOINT_DIR = path.join(mockPreDir, 'agent-checkpoints');

beforeAll(() => {
  fs.mkdirSync(mockPreDir, { recursive: true });
  fs.mkdirSync(path.join(mockPreDir, 'sessions'), { recursive: true });
});

afterAll(() => {
  fs.rmSync(mockPreDir, { recursive: true, force: true });
});

beforeEach(() => {
  // Clean state between tests
  try { fs.unlinkSync(STATE_FILE); } catch (_e) {}
  try { fs.rmSync(CHECKPOINT_DIR, { recursive: true, force: true }); } catch (_e) {}
  jest.resetModules();
});


describe('Background Agent State Persistence', () => {
  test('spawn persists agent state to disk', async () => {
    const bgAgents = require('../src/background-agents');
    bgAgents.setBroadcast(() => {});

    const result = bgAgents.spawn({ task: 'Test task', name: 'Test Agent' });
    expect(result.id).toMatch(/^bg_/);
    expect(result.status).toBe('queued');

    // Wait for async execution
    await new Promise(r => setTimeout(r, 200));

    // Verify state file was written
    expect(fs.existsSync(STATE_FILE)).toBe(true);
    const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
    expect(state.agents).toBeInstanceOf(Array);
    expect(state.agents.length).toBeGreaterThanOrEqual(1);

    const persisted = state.agents.find(a => a.id === result.id);
    expect(persisted).toBeTruthy();
    expect(persisted.task).toBe('Test task');
    expect(persisted.name).toBe('Test Agent');

    bgAgents.shutdown();
  });

  test('agent state includes all lifecycle fields', async () => {
    const bgAgents = require('../src/background-agents');
    bgAgents.setBroadcast(() => {});

    const result = bgAgents.spawn({ task: 'Lifecycle test' });
    await new Promise(r => setTimeout(r, 200));

    const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
    const agent = state.agents.find(a => a.id === result.id);
    expect(agent).toBeTruthy();
    expect(agent).toHaveProperty('id');
    expect(agent).toHaveProperty('name');
    expect(agent).toHaveProperty('task');
    expect(agent).toHaveProperty('sessionId');
    expect(agent).toHaveProperty('status');
    expect(agent).toHaveProperty('createdAt');
    expect(agent).toHaveProperty('tokens');

    bgAgents.shutdown();
  });

  test('listAgents returns all spawned agents', async () => {
    const bgAgents = require('../src/background-agents');
    bgAgents.setBroadcast(() => {});

    bgAgents.spawn({ task: 'Task A', name: 'Agent A' });
    bgAgents.spawn({ task: 'Task B', name: 'Agent B' });
    await new Promise(r => setTimeout(r, 400));

    const agents = bgAgents.listAgents();
    expect(agents.length).toBe(2);
    const names = agents.map(a => a.name);
    expect(names).toContain('Agent A');
    expect(names).toContain('Agent B');

    bgAgents.shutdown();
  });

  test('cancel persists updated status', async () => {
    const bgAgents = require('../src/background-agents');
    const { runToolLoop } = require('../src/tools');
    bgAgents.setBroadcast(() => {});

    // Make tool loop hang so agent stays running
    runToolLoop.mockImplementation(async ({ signal }) => {
      await new Promise((_resolve, reject) => {
        signal.addEventListener('abort', () => reject(new Error('Aborted')));
      });
    });

    const result = bgAgents.spawn({ task: 'Cancel test' });
    await new Promise(r => setTimeout(r, 150));

    bgAgents.cancelAgent(result.id);
    await new Promise(r => setTimeout(r, 150));

    const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
    const agent = state.agents.find(a => a.id === result.id);
    expect(agent.status).toBe('cancelled');

    bgAgents.shutdown();
  });
});


describe('Turn-Level Checkpointing', () => {
  test('onCheckpoint callback receives turn number', async () => {
    const bgAgents = require('../src/background-agents');
    const { runToolLoop } = require('../src/tools');
    bgAgents.setBroadcast(() => {});

    // Mock tool loop that calls onCheckpoint
    runToolLoop.mockImplementation(async ({ send, onCheckpoint }) => {
      send({ type: 'tool_call', name: 'test_tool' });
      send({ type: 'tool_result' });
      if (onCheckpoint) onCheckpoint(1);
      send({ type: 'token', content: 'Done' });
      send({ type: 'done', stats: { prompt_eval_count: 100, eval_count: 50 } });
    });

    const result = bgAgents.spawn({ task: 'Checkpoint test' });
    await new Promise(r => setTimeout(r, 250));

    // Agent should have completed successfully
    const agent = bgAgents.getAgent(result.id);
    expect(agent.status).toBe('completed');

    bgAgents.shutdown();
  });

  test('checkpoint file exists during mid-execution', async () => {
    const bgAgents = require('../src/background-agents');
    const { runToolLoop } = require('../src/tools');
    bgAgents.setBroadcast(() => {});

    let midRunCheck = false;

    runToolLoop.mockImplementation(async ({ send, onCheckpoint }) => {
      send({ type: 'tool_call', name: 'step1' });
      if (onCheckpoint) onCheckpoint(1);

      // Check that checkpoint file exists during execution
      const agents = bgAgents.listAgents();
      if (agents.length > 0) {
        const cpFile = path.join(CHECKPOINT_DIR, `${agents[0].id}.json`);
        midRunCheck = fs.existsSync(cpFile);
      }

      send({ type: 'token', content: 'Done' });
      send({ type: 'done', stats: {} });
    });

    bgAgents.spawn({ task: 'Mid-run checkpoint test' });
    await new Promise(r => setTimeout(r, 300));

    expect(midRunCheck).toBe(true);

    bgAgents.shutdown();
  });

  test('checkpoint file is cleared on successful completion', async () => {
    const bgAgents = require('../src/background-agents');
    const { runToolLoop } = require('../src/tools');
    bgAgents.setBroadcast(() => {});

    runToolLoop.mockImplementation(async ({ send, onCheckpoint }) => {
      if (onCheckpoint) onCheckpoint(1);
      if (onCheckpoint) onCheckpoint(2);
      send({ type: 'token', content: 'Done' });
      send({ type: 'done', stats: {} });
    });

    const result = bgAgents.spawn({ task: 'Cleanup test' });
    await new Promise(r => setTimeout(r, 250));

    const cpFile = path.join(CHECKPOINT_DIR, `${result.id}.json`);
    expect(fs.existsSync(cpFile)).toBe(false);

    bgAgents.shutdown();
  });
});


describe('Startup Recovery', () => {
  test('init recovers interrupted agents from disk', () => {
    // Pre-seed state file with a "running" agent (simulating crash)
    fs.mkdirSync(CHECKPOINT_DIR, { recursive: true });
    const fakeAgent = {
      id: 'bg_99',
      name: 'Crashed Agent',
      task: 'Important task that was interrupted',
      sessionId: 'test-session-crashed',
      cwd: '/tmp',
      status: 'running',
      createdAt: Date.now() - 60000,
      startedAt: Date.now() - 50000,
      completedAt: null,
      result: null,
      error: null,
      toolCalls: 3,
      tokens: { in: 500, out: 200 },
      checkpoint: { turn: 2, savedAt: Date.now() - 45000 },
    };
    fs.writeFileSync(STATE_FILE, JSON.stringify({ agents: [fakeAgent], seq: 99 }));
    fs.writeFileSync(
      path.join(CHECKPOINT_DIR, 'bg_99.json'),
      JSON.stringify({ agentId: 'bg_99', turn: 2, savedAt: Date.now() - 45000 })
    );

    const bgAgents = require('../src/background-agents');
    bgAgents.setBroadcast(() => {});

    const recovery = bgAgents.init();
    expect(recovery.recovered).toBe(1);

    // Agent should be re-queued
    const agents = bgAgents.listAgents();
    const recovered = agents.find(a => a.id === 'bg_99');
    expect(recovered).toBeTruthy();
    expect(['queued', 'running', 'completed']).toContain(recovered.status);

    bgAgents.shutdown();
  });

  test('init with no state file returns zero recovered', () => {
    const bgAgents = require('../src/background-agents');
    bgAgents.setBroadcast(() => {});

    const recovery = bgAgents.init();
    expect(recovery.recovered).toBe(0);

    bgAgents.shutdown();
  });

  test('completed agents in state file are loaded to history', () => {
    const completedAgent = {
      id: 'bg_50',
      name: 'Old Agent',
      task: 'Already done',
      sessionId: 'test-session-old',
      cwd: '/tmp',
      status: 'completed',
      createdAt: Date.now() - 120000,
      startedAt: Date.now() - 110000,
      completedAt: Date.now() - 100000,
      result: 'All done',
      error: null,
      toolCalls: 5,
      tokens: { in: 1000, out: 500 },
    };
    fs.writeFileSync(STATE_FILE, JSON.stringify({ agents: [completedAgent], seq: 50 }));

    const bgAgents = require('../src/background-agents');
    bgAgents.setBroadcast(() => {});

    const recovery = bgAgents.init();
    expect(recovery.recovered).toBe(0);

    const agents = bgAgents.listAgents();
    const old = agents.find(a => a.id === 'bg_50');
    expect(old).toBeTruthy();
    expect(old.status).toBe('completed');

    bgAgents.shutdown();
  });

  test('resume passes skipTurns from checkpoint', async () => {
    const { runToolLoop } = require('../src/tools');
    let capturedSkipTurns = null;
    runToolLoop.mockImplementation(async ({ skipTurns, send }) => {
      capturedSkipTurns = skipTurns;
      send({ type: 'token', content: 'Resumed' });
      send({ type: 'done', stats: {} });
    });

    // Pre-seed with interrupted agent at turn 3
    fs.mkdirSync(CHECKPOINT_DIR, { recursive: true });
    fs.writeFileSync(STATE_FILE, JSON.stringify({
      agents: [{
        id: 'bg_77',
        name: 'Resume Test',
        task: 'Resume from turn 3',
        sessionId: 'test-session-resume',
        cwd: '/tmp',
        status: 'running',
        createdAt: Date.now(),
        startedAt: Date.now(),
        completedAt: null,
        result: null,
        error: null,
        toolCalls: 3,
        tokens: { in: 0, out: 0 },
        checkpoint: { turn: 3 },
      }],
      seq: 77,
    }));
    fs.writeFileSync(
      path.join(CHECKPOINT_DIR, 'bg_77.json'),
      JSON.stringify({ agentId: 'bg_77', turn: 3 })
    );

    const bgAgents = require('../src/background-agents');
    bgAgents.setBroadcast(() => {});
    bgAgents.init();

    await new Promise(r => setTimeout(r, 400));

    expect(capturedSkipTurns).toBe(3);

    bgAgents.shutdown();
  });
});


describe('Queue Status', () => {
  test('getQueueStatus includes all status counts', () => {
    const bgAgents = require('../src/background-agents');
    bgAgents.setBroadcast(() => {});

    const status = bgAgents.getQueueStatus();
    expect(status).toHaveProperty('paused');
    expect(status).toHaveProperty('interrupted');
    expect(status).toHaveProperty('queued');
    expect(status).toHaveProperty('running');
    expect(status).toHaveProperty('completed');
    expect(status).toHaveProperty('failed');
    expect(status).toHaveProperty('cancelled');
    expect(status).toHaveProperty('total');

    bgAgents.shutdown();
  });
});


describe('Per-spawn timeout override', () => {
  test('defaults to 10 minutes when timeoutMinutes is omitted', async () => {
    const bgAgents = require('../src/background-agents');
    bgAgents.setBroadcast(() => {});
    const result = bgAgents.spawn({ task: 'Default timeout', name: 'Default' });
    expect(result.timeoutMinutes).toBe(10);
    const agent = bgAgents.getAgent(result.id);
    expect(agent.timeoutMinutes).toBe(10);
    bgAgents.shutdown();
  });

  test('accepts a custom timeoutMinutes value within range', async () => {
    const bgAgents = require('../src/background-agents');
    bgAgents.setBroadcast(() => {});
    const result = bgAgents.spawn({ task: 'Long task', name: 'Long', timeoutMinutes: 25 });
    expect(result.timeoutMinutes).toBe(25);
    expect(bgAgents.getAgent(result.id).timeoutMinutes).toBe(25);
    bgAgents.shutdown();
  });

  test('clamps values above 60 minutes down to the max', async () => {
    const bgAgents = require('../src/background-agents');
    bgAgents.setBroadcast(() => {});
    const result = bgAgents.spawn({ task: 'Too long', timeoutMinutes: 999 });
    expect(result.timeoutMinutes).toBe(60);
    bgAgents.shutdown();
  });

  test('clamps zero and negative values up to the minimum', async () => {
    const bgAgents = require('../src/background-agents');
    bgAgents.setBroadcast(() => {});
    const zero = bgAgents.spawn({ task: 'Zero', timeoutMinutes: 0 });
    const neg = bgAgents.spawn({ task: 'Neg', timeoutMinutes: -5 });
    // Falsy/non-positive values fall back to the 10-minute default rather than the
    // 1-minute floor — we want "left blank" to behave the same as "omitted".
    expect(zero.timeoutMinutes).toBe(10);
    expect(neg.timeoutMinutes).toBe(10);
    bgAgents.shutdown();
  });

  test('ignores non-numeric values and falls back to default', async () => {
    const bgAgents = require('../src/background-agents');
    bgAgents.setBroadcast(() => {});
    const result = bgAgents.spawn({ task: 'Bad input', timeoutMinutes: 'twenty' });
    expect(result.timeoutMinutes).toBe(10);
    bgAgents.shutdown();
  });

  test('timeoutMinutes is persisted to disk and survives reload', async () => {
    const bgAgents = require('../src/background-agents');
    bgAgents.setBroadcast(() => {});
    const result = bgAgents.spawn({ task: 'Persist me', timeoutMinutes: 30 });
    await new Promise(r => setTimeout(r, 100));
    bgAgents.shutdown();

    const persisted = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
    const saved = persisted.agents.find(a => a.id === result.id);
    expect(saved).toBeTruthy();
    expect(saved.timeoutMinutes).toBe(30);
  });

  test('listAgents surfaces timeoutMinutes for the UI', async () => {
    const bgAgents = require('../src/background-agents');
    bgAgents.setBroadcast(() => {});
    bgAgents.spawn({ task: 'A', timeoutMinutes: 15 });
    bgAgents.spawn({ task: 'B' /* default */ });
    const list = bgAgents.listAgents();
    const tm = list.map(a => a.timeoutMinutes).sort();
    expect(tm).toEqual([10, 15]);
    bgAgents.shutdown();
  });
});
