/**
 * Unit tests for `_resolveModelApiType` + `_pickSystemPromptForModel`
 * on AgentScheduler — the runtime-side half of the "trim duplicated
 * tool docs for Responses-API models" feature.
 *
 * Three concerns covered:
 *   1. Classification (_resolveModelApiType) — every catalog shape the
 *      backend's _inferRouting recognizes must produce the same answer
 *      on the CLI side, or the optimization fires on the wrong models.
 *   2. Selection (_pickSystemPromptForModel) — must fall back to
 *      `agent.systemPrompt` for every safety-net path:
 *         • modelsService missing                → fallback
 *         • model not in catalog                 → fallback
 *         • apiType resolves to chat_completion → fallback
 *         • originalSystemPrompt missing         → fallback
 *         • toolsRegistry missing                → fallback
 *         • rebuild throws                       → fallback
 *      Only the happy path (Responses model + everything available)
 *      returns the trimmed rebuild.
 *   3. Caching — same (agent, model) twice should rebuild ONCE; the
 *      invalidator should clear entries for ONE agent only.
 */
import { jest, describe, test, expect, beforeEach } from '@jest/globals';

// Stub the activity service so importing the scheduler doesn't drag in
// the full agent dependency graph for these unit tests.
jest.unstable_mockModule('../../services/agentActivityService.js', () => ({
  shouldAgentBeActive: () => ({ active: false, reason: 'stub' }),
  getActiveAgents: () => [],
  shouldSkipIteration: () => false,
}));

const { default: AgentScheduler } = await import('../agentScheduler.js');

// ─── Test-only helpers ────────────────────────────────────────────────
function makeScheduler({ models = [], registry = null, logger = null } = {}) {
  const modelsService = {
    getModels: () => models,
  };
  const agentPool = {
    toolsRegistry: registry,
    getAllAgents: async () => new Map(),
    getAgent: async () => null,
  };
  const aiService = {};
  const messageProcessor = {};
  const log = logger || {
    info() {}, warn() {}, error() {}, debug() {},
  };
  return new AgentScheduler(
    agentPool,
    messageProcessor,
    aiService,
    log,
    null,           // webSocketManager
    null,           // modelRouterService
    modelsService,  // ← what we care about
  );
}

// A minimal fake registry that records the apiType it was called with
// so we can assert the scheduler propagates it correctly.
function makeRegistry({ shouldThrow = false } = {}) {
  const calls = [];
  return {
    calls,
    enhanceSystemPrompt(prompt, capabilities, options) {
      calls.push({ prompt, capabilities, options });
      if (shouldThrow) throw new Error('boom');
      const apiTag = options?.apiType === 'responses' ? '[trimmed]' : '[full]';
      return `${prompt}\n## TOOLS ${apiTag} for caps=${(capabilities || []).join(',')}`;
    },
  };
}

// ──────────────────────────────────────────────────────────────────────
// 1. _resolveModelApiType — parity with backend's _inferRouting
// ──────────────────────────────────────────────────────────────────────

describe('_resolveModelApiType — catalog → routing decision', () => {
  test('api_type=["responses"] alone → "responses"', () => {
    const s = makeScheduler({ models: [{ name: 'codex-mini', api_type: ['responses'] }] });
    expect(s._resolveModelApiType('codex-mini')).toBe('responses');
  });

  test('api_type=["chat_completion","responses"] (BOTH present) → "chat_completion"', () => {
    // Backend rule: only flips to responses when chat_completion is ABSENT.
    const s = makeScheduler({
      models: [{ name: 'gpt-5', api_type: ['chat_completion', 'responses'] }],
    });
    expect(s._resolveModelApiType('gpt-5')).toBe('chat_completion');
  });

  test('api_type=["chat_completion"] → "chat_completion"', () => {
    const s = makeScheduler({ models: [{ name: 'claude', api_type: ['chat_completion'] }] });
    expect(s._resolveModelApiType('claude')).toBe('chat_completion');
  });

  test('capabilities.responses=="true" + chatCompletion=="false" → "responses"', () => {
    const s = makeScheduler({
      models: [{
        name: 'o3',
        api_type: ['responses'],
        capabilities: { responses: 'true', chatCompletion: 'false' },
      }],
    });
    expect(s._resolveModelApiType('o3')).toBe('responses');
  });

  test('explicit useResponsesApi flag → "responses"', () => {
    const s = makeScheduler({
      models: [{ name: 'foo', api_type: [], useResponsesApi: true }],
    });
    expect(s._resolveModelApiType('foo')).toBe('responses');
  });

  test('name-pattern fallback: "codex" → "responses" even with no catalog data', () => {
    const s = makeScheduler({ models: [{ name: 'gpt-5-1-codex-mini' }] });
    expect(s._resolveModelApiType('gpt-5-1-codex-mini')).toBe('responses');
  });

  test('name-pattern fallback: "gpt-X-pro" → "responses"', () => {
    const s = makeScheduler({ models: [{ name: 'gpt-5-pro' }] });
    expect(s._resolveModelApiType('gpt-5-pro')).toBe('responses');
  });

  test('unknown model returns undefined (caller falls back to old behaviour)', () => {
    const s = makeScheduler({ models: [{ name: 'claude', api_type: ['chat_completion'] }] });
    expect(s._resolveModelApiType('mystery-model')).toBeUndefined();
  });

  test('modelsService missing → undefined (safe — caller falls back)', () => {
    const s = new AgentScheduler({ toolsRegistry: null }, {}, {}, {
      info() {}, warn() {}, error() {}, debug() {},
    });
    expect(s._resolveModelApiType('codex-mini')).toBeUndefined();
  });

  test('arbitrary exception in catalog → undefined (defensive)', () => {
    // getModels() throws — must not propagate.
    const s = new AgentScheduler(
      { toolsRegistry: null },
      {},
      {},
      { info() {}, warn() {}, error() {}, debug() {} },
      null,
      null,
      { getModels() { throw new Error('catalog offline'); } },
    );
    expect(s._resolveModelApiType('codex-mini')).toBeUndefined();
  });
});

// ──────────────────────────────────────────────────────────────────────
// 2. _pickSystemPromptForModel — happy path + every safety-net path
// ──────────────────────────────────────────────────────────────────────

describe('_pickSystemPromptForModel — back-compat fallbacks', () => {
  const BAKED_PROMPT = 'BAKED: agent persona\n## AVAILABLE TOOLS\n…lots of text…';
  const ORIGINAL_PROMPT = 'Agent persona';

  const agentFor = (overrides = {}) => ({
    id: 'agent-1',
    systemPrompt: BAKED_PROMPT,
    originalSystemPrompt: ORIGINAL_PROMPT,
    capabilities: ['memory', 'terminal'],
    skills: [],
    ...overrides,
  });

  test('chat-completion model → returns agent.systemPrompt verbatim', async () => {
    const s = makeScheduler({
      models: [{ name: 'claude', api_type: ['chat_completion'] }],
      registry: makeRegistry(),
    });
    const out = await s._pickSystemPromptForModel(agentFor(), 'claude');
    expect(out).toBe(BAKED_PROMPT);
  });

  test('unknown model → returns agent.systemPrompt verbatim', async () => {
    const s = makeScheduler({ models: [], registry: makeRegistry() });
    const out = await s._pickSystemPromptForModel(agentFor(), 'never-heard-of');
    expect(out).toBe(BAKED_PROMPT);
  });

  test('Responses model BUT originalSystemPrompt missing → fallback', async () => {
    // Very old persisted agent (pre-originalSystemPrompt storage).
    const s = makeScheduler({
      models: [{ name: 'codex', api_type: ['responses'] }],
      registry: makeRegistry(),
    });
    const out = await s._pickSystemPromptForModel(
      agentFor({ originalSystemPrompt: undefined }),
      'codex',
    );
    expect(out).toBe(BAKED_PROMPT);
  });

  test('Responses model BUT toolsRegistry missing → fallback', async () => {
    const s = makeScheduler({
      models: [{ name: 'codex', api_type: ['responses'] }],
      registry: null,
    });
    const out = await s._pickSystemPromptForModel(agentFor(), 'codex');
    expect(out).toBe(BAKED_PROMPT);
  });

  test('Responses model AND rebuild throws → fallback (no crash)', async () => {
    const s = makeScheduler({
      models: [{ name: 'codex', api_type: ['responses'] }],
      registry: makeRegistry({ shouldThrow: true }),
    });
    const out = await s._pickSystemPromptForModel(agentFor(), 'codex');
    expect(out).toBe(BAKED_PROMPT);
  });
});

// ──────────────────────────────────────────────────────────────────────
// 3. _pickSystemPromptForModel — happy path
// ──────────────────────────────────────────────────────────────────────

describe('_pickSystemPromptForModel — Responses model rebuild', () => {
  const agent = {
    id: 'agent-1',
    systemPrompt: 'STALE (chat-completion shape)',
    originalSystemPrompt: 'Agent persona',
    capabilities: ['memory', 'terminal'],
    skills: [],
  };

  test('rebuilds with apiType:"responses" and returns trimmed prompt', async () => {
    const reg = makeRegistry();
    const s = makeScheduler({
      models: [{ name: 'codex', api_type: ['responses'] }],
      registry: reg,
    });
    const out = await s._pickSystemPromptForModel(agent, 'codex');
    expect(out).toContain('[trimmed]');
    expect(out).toContain('Agent persona');
    // The registry was called with apiType: 'responses' AND the agent's capabilities.
    expect(reg.calls).toHaveLength(1);
    expect(reg.calls[0].options.apiType).toBe('responses');
    expect(reg.calls[0].capabilities).toEqual(['memory', 'terminal']);
  });

  test('caches per (agentId, modelName) — second call does NOT rebuild', async () => {
    const reg = makeRegistry();
    const s = makeScheduler({
      models: [{ name: 'codex', api_type: ['responses'] }],
      registry: reg,
    });
    await s._pickSystemPromptForModel(agent, 'codex');
    await s._pickSystemPromptForModel(agent, 'codex');
    await s._pickSystemPromptForModel(agent, 'codex');
    expect(reg.calls).toHaveLength(1);
  });

  test('different models for same agent → SEPARATE cache entries', async () => {
    const reg = makeRegistry();
    const s = makeScheduler({
      models: [
        { name: 'codex', api_type: ['responses'] },
        { name: 'gpt-5-pro', api_type: ['responses'] },
      ],
      registry: reg,
    });
    await s._pickSystemPromptForModel(agent, 'codex');
    await s._pickSystemPromptForModel(agent, 'gpt-5-pro');
    expect(reg.calls).toHaveLength(2);
  });

  test('switching back to chat-completion mid-session uses the persisted prompt unchanged', async () => {
    const reg = makeRegistry();
    const s = makeScheduler({
      models: [
        { name: 'codex',  api_type: ['responses'] },
        { name: 'claude', api_type: ['chat_completion'] },
      ],
      registry: reg,
    });
    const native = await s._pickSystemPromptForModel(agent, 'codex');
    const inline = await s._pickSystemPromptForModel(agent, 'claude');
    expect(native).toContain('[trimmed]');
    expect(inline).toBe('STALE (chat-completion shape)');  // persisted, untouched
    expect(reg.calls).toHaveLength(1);                     // only the codex rebuild ran
  });
});

// ──────────────────────────────────────────────────────────────────────
// 4. Cache invalidation
// ──────────────────────────────────────────────────────────────────────

describe('_invalidateNativePromptCache — selective per-agent clear', () => {
  test('clears entries for ONE agent only, leaves others alone', async () => {
    const reg = makeRegistry();
    const s = makeScheduler({
      models: [{ name: 'codex', api_type: ['responses'] }],
      registry: reg,
    });
    const a1 = { id: 'a1', systemPrompt: 'p1', originalSystemPrompt: 'persona-1', capabilities: ['memory'], skills: [] };
    const a2 = { id: 'a2', systemPrompt: 'p2', originalSystemPrompt: 'persona-2', capabilities: ['terminal'], skills: [] };

    await s._pickSystemPromptForModel(a1, 'codex');
    await s._pickSystemPromptForModel(a2, 'codex');
    expect(reg.calls).toHaveLength(2);

    s._invalidateNativePromptCache('a1');

    // Re-fetching a1 → rebuild. a2 → still cached.
    await s._pickSystemPromptForModel(a1, 'codex');
    await s._pickSystemPromptForModel(a2, 'codex');
    expect(reg.calls).toHaveLength(3); // only a1 rebuilt
  });

  test('invalidating for an agent that never rendered is a no-op (does not throw)', async () => {
    const s = makeScheduler({ models: [], registry: makeRegistry() });
    expect(() => s._invalidateNativePromptCache('never-seen')).not.toThrow();
  });
});
