import { describe, it, expect, vi } from 'vitest';
import { CouncilsOrchestrator, type CouncilsConfig } from './orchestrator.js';
import type { AgentConfigRegistry } from '../agents/registry.js';

function createRegistry(): AgentConfigRegistry {
  const configs = new Map<string, { name: string; modelTier?: 'fast' | 'default' | 'complex' | 'local'; systemPrompt?: string }>([
    ['council_d_arbiter', { name: 'council_d_arbiter', modelTier: 'default', systemPrompt: 'D Arbiter' }],
    ['council_d_freethinker', { name: 'council_d_freethinker', modelTier: 'default', systemPrompt: 'D FT' }],
    ['council_p_arbiter', { name: 'council_p_arbiter', modelTier: 'default', systemPrompt: 'P Arbiter' }],
    ['council_p_freethinker', { name: 'council_p_freethinker', modelTier: 'default', systemPrompt: 'P FT' }],
    ['council_meta_arbiter', { name: 'council_meta_arbiter', modelTier: 'default', systemPrompt: 'Meta' }],
  ]);

  return {
    get: (name: string) => configs.get(name),
    list: () => [...configs.values()],
  } as unknown as AgentConfigRegistry;
}

function createConfig(overrides?: Partial<CouncilsConfig>): CouncilsConfig {
  return {
    enabled: true,
    defaults: {
      max_rounds: 2,
      ideas_per_round: 3,
      top_ideas_for_bridge: 2,
      bridge_packet_max_chars: 4000,
      bridge_field_max_bullets: 6,
      bridge_entry_max_chars: 300,
      novelty_delta_threshold: 10,
      repetition_threshold: 70,
    },
    strict_grounding: false,
    strict_meta_validation: true,
    groups: {
      D: {
        arbiter_agent: 'council_d_arbiter',
        freethinker_agent: 'council_d_freethinker',
        group_prompt_prefix: 'D prefix',
        novelty_bias: 'low',
        risk_tolerance: 'low',
        forbidden_approaches: ['moonshots'],
      },
      P: {
        arbiter_agent: 'council_p_arbiter',
        freethinker_agent: 'council_p_freethinker',
        group_prompt_prefix: 'P prefix',
        novelty_bias: 'high',
        risk_tolerance: 'high',
        forbidden_approaches: ['incremental'],
      },
    },
    meta_arbiter_agent: 'council_meta_arbiter',
    ...overrides,
  };
}

describe('CouncilsOrchestrator', () => {
  it('runs D/P pipeline with deterministic IDs and trace ordering', async () => {
    const delegate = vi.fn(async ({ message }: { message: string }) => {
      const payload = JSON.parse(message);

      if (payload.brief_D && payload.brief_P) {
        return {
          content: JSON.stringify({
            schema_version: '1.0.0',
            selected_primary: [payload.brief_D.shortlist[0]],
            selected_secondary: [payload.brief_P.shortlist[0]],
            merges: [],
            rejections: [],
            open_questions: ['q1'],
            next_experiments: ['e1'],
          }),
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }

      if (payload.shortlisted_ideas) {
        return {
          content: JSON.stringify({
            grounded: payload.shortlisted_ideas.map((idea: { idea_id: string }) => ({
              idea_id: idea.idea_id,
              mve: `test-${idea.idea_id}`,
              constraints: ['c1'],
              falsifiability_checks: ['f1'],
            })),
          }),
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }

      if (payload.ideas) {
        const ids = payload.ideas.map((idea: { idea_id: string }) => idea.idea_id);
        const round = payload.round;
        return {
          content: JSON.stringify({
            assessments: ids.map((idea_id: string, i: number) => ({
              idea_id,
              scores: { novelty: 60 - i, feasibility: 80, impact: 70, testability: 75 },
              decision: i === 0 ? 'shortlist' : 'hold',
              notes: `note-${idea_id}`,
            })),
            assumptions: [`assume-${payload.group}-${round}`],
            risks: [`risk-${payload.group}-${round}`],
            asks: [`ask-${payload.group}-${round}`],
            what_to_steal: [`steal-${payload.group}-${round}`],
            convergence_signal: round >= 2,
            novelty_score: round >= 2 ? 50 : 62,
            repetition_rate: round >= 2 ? 72 : 20,
          }),
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }

      if (payload.group && payload.round) {
        const marker = payload.group === 'D' ? 'D-MARKER' : 'P-MARKER';
        return {
          content: JSON.stringify({
            ideas: [
              {
                title: `${payload.group} idea 1`,
                hypothesis: `${marker} hypothesis`,
                mechanism: `${marker} mechanism`,
                expected_outcome: 'Outcome 1',
              },
              {
                title: `${payload.group} idea 2`,
                hypothesis: `h2-${payload.group}`,
                mechanism: `m2-${payload.group}`,
                expected_outcome: 'Outcome 2',
              },
            ],
          }),
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }

      throw new Error('Unexpected payload');
    });

    const orchestrator = new CouncilsOrchestrator({
      registry: createRegistry(),
      orchestrator: { delegate },
      config: createConfig(),
    });

    const result = await orchestrator.run({ task: 'design test harness' });
    expect(result.pipeline_version).toBe('1.0.0');
    expect(result.brief_D_v1.ideas[0].idea_id).toBe('D.r1.01');
    expect(result.brief_P_v1.ideas[0].idea_id).toBe('P.r1.01');
    expect(typeof result.trace[0]?.latency_ms).toBe('number');
    expect(result.stop_snapshot.stop_reason).toBe('convergence');
    const callIds = result.trace.map((e) => e.call_id);
    expect(callIds).toEqual([...callIds].sort((a, b) => {
      const pa = result.trace.find((e) => e.call_id === a)?.phase_index ?? 0;
      const pb = result.trace.find((e) => e.call_id === b)?.phase_index ?? 0;
      return pa - pb || a.localeCompare(b);
    }));
  });

  it('keeps cross-council leakage constrained to bridge fields', async () => {
    const observedPayloads: unknown[] = [];
    const delegate = vi.fn(async ({ message }: { message: string }) => {
      const payload = JSON.parse(message);
      observedPayloads.push(payload);

      if (payload.brief_D && payload.brief_P) {
        return {
          content: JSON.stringify({
            schema_version: '1.0.0',
            selected_primary: [payload.brief_D.shortlist[0]],
            selected_secondary: [payload.brief_P.shortlist[0]],
            merges: [],
            rejections: [],
            open_questions: ['q1'],
            next_experiments: ['e1'],
          }),
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }

      if (payload.shortlisted_ideas) {
        return {
          content: JSON.stringify({ grounded: [] }),
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }

      if (payload.ideas) {
        return {
          content: JSON.stringify({
            assessments: payload.ideas.map((idea: { idea_id: string }, idx: number) => ({
              idea_id: idea.idea_id,
              scores: { novelty: 50, feasibility: 50, impact: 50, testability: 50 },
              decision: idx === 0 ? 'shortlist' : 'hold',
              notes: 'ok',
            })),
            assumptions: [],
            risks: [],
            asks: [],
            what_to_steal: [],
            convergence_signal: true,
            novelty_score: 50,
            repetition_rate: 90,
          }),
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }

      return {
        content: JSON.stringify({
          ideas: [
            {
              title: 't',
              hypothesis: payload.group === 'D' ? 'D-MARKER hypothesis' : 'plain hypothesis',
              mechanism: payload.group === 'D' ? 'D-MARKER mechanism' : 'plain mechanism',
              expected_outcome: 'o',
            },
          ],
        }),
        usage: { inputTokens: 10, outputTokens: 5 },
        tier: 'default' as const,
      };
    });

    const orchestrator = new CouncilsOrchestrator({
      registry: createRegistry(),
      orchestrator: { delegate },
      config: createConfig(),
    });

    await orchestrator.run({ task: 'x' });

    const pRound1Payloads = observedPayloads.filter((p: any) => p.group === 'P' && p.round === 1);
    expect(JSON.stringify(pRound1Payloads)).not.toContain('D-MARKER');

    const pRound2Payload = observedPayloads.find((p: any) => p.group === 'P' && p.round === 2) as any;
    expect(JSON.stringify(pRound2Payload.peer_bridge)).toContain('D-MARKER');
  });

  it('requests json_schema response format for council structured phases', async () => {
    const delegate = vi.fn(async ({ message }: { message: string }) => {
      const payload = JSON.parse(message);

      if (payload.brief_D && payload.brief_P) {
        return {
          content: JSON.stringify({
            schema_version: '1.0.0',
            selected_primary: [payload.brief_D.shortlist[0]],
            selected_secondary: [payload.brief_P.shortlist[0]],
            merges: [],
            rejections: [],
            open_questions: ['q1'],
            next_experiments: ['e1'],
          }),
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }

      if (payload.shortlisted_ideas) {
        return {
          content: JSON.stringify({
            grounded: payload.shortlisted_ideas.map((idea: { idea_id: string }) => ({
              idea_id: idea.idea_id,
              mve: `mve-${idea.idea_id}`,
              constraints: ['c1'],
              falsifiability_checks: ['f1'],
            })),
          }),
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }

      if (payload.ideas) {
        return {
          content: JSON.stringify({
            assessments: payload.ideas.map((idea: { idea_id: string }, idx: number) => ({
              idea_id: idea.idea_id,
              scores: { novelty: 60, feasibility: 70, impact: 80, testability: 90 },
              decision: idx === 0 ? 'shortlist' : 'hold',
              notes: 'ok',
            })),
            assumptions: ['a1'],
            risks: ['r1'],
            asks: ['q1'],
            what_to_steal: ['w1'],
            convergence_signal: false,
            novelty_score: 60,
            repetition_rate: 10,
          }),
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }

      return {
        content: JSON.stringify({
          ideas: [
            { title: 't1', hypothesis: 'h1', mechanism: 'm1', expected_outcome: 'o1' },
            { title: 't2', hypothesis: 'h2', mechanism: 'm2', expected_outcome: 'o2' },
          ],
        }),
        usage: { inputTokens: 10, outputTokens: 5 },
        tier: 'default' as const,
      };
    });

    const orchestrator = new CouncilsOrchestrator({
      registry: createRegistry(),
      orchestrator: { delegate },
      config: createConfig({ defaults: { ...createConfig().defaults, max_rounds: 1 } }),
    });

    await orchestrator.run({ task: 'json schema check' });

    const schemaNames = delegate.mock.calls
      .map((call) => (call[0] as { responseFormat?: { type: string; name?: string } }).responseFormat)
      .filter((rf): rf is { type: string; name?: string } => Boolean(rf))
      .filter((rf) => rf.type === 'json_schema')
      .map((rf) => rf.name);

    expect(schemaNames).toContain('council_ideation');
    expect(schemaNames).toContain('council_assessment');
    expect(schemaNames).toContain('council_grounding');
    expect(schemaNames).toContain('council_meta');

    const metaResponseFormat = delegate.mock.calls
      .map((call) => (call[0] as { responseFormat?: Record<string, unknown> }).responseFormat)
      .find((rf) => rf && rf.type === 'json_schema' && rf.name === 'council_meta') as
      | { schema?: { required?: string[] } }
      | undefined;
    expect(metaResponseFormat?.schema?.required).toContain('merges');
  });

  it('fails closed on bridge cap overflow before phase 2 executes', async () => {
    const delegate = vi.fn(async ({ message }: { message: string }) => {
      const payload = JSON.parse(message);
      if (payload.brief_D && payload.brief_P) {
        return {
          content: JSON.stringify({
            schema_version: '1.0.0',
            selected_primary: [payload.brief_D.shortlist[0]],
            selected_secondary: [payload.brief_P.shortlist[0]],
            merges: [],
            rejections: [],
            open_questions: ['q1'],
            next_experiments: ['e1'],
          }),
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }
      if (payload.shortlisted_ideas) {
        return {
          content: JSON.stringify({ grounded: payload.shortlisted_ideas.map((idea: any) => ({ idea_id: idea.idea_id, mve: 'm', constraints: ['c'], falsifiability_checks: ['f'] })) }),
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }
      if (payload.ideas) {
        return {
          content: JSON.stringify({
            assessments: payload.ideas.map((idea: any, idx: number) => ({
              idea_id: idea.idea_id,
              scores: { novelty: 50, feasibility: 50, impact: 50, testability: 50 },
              decision: idx === 0 ? 'shortlist' : 'hold',
              notes: 'very-long-note-that-will-overflow-bridge-limits',
            })),
            assumptions: ['a'],
            risks: ['r'],
            asks: ['k'],
            what_to_steal: ['this-is-way-too-long-for-cap'],
            convergence_signal: false,
            novelty_score: 60,
            repetition_rate: 10,
          }),
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }
      return {
        content: JSON.stringify({
          ideas: [{ title: 't', hypothesis: 'h', mechanism: 'm', expected_outcome: 'o' }],
        }),
        usage: { inputTokens: 10, outputTokens: 5 },
        tier: 'default' as const,
      };
    });

    const orchestrator = new CouncilsOrchestrator({
      registry: createRegistry(),
      orchestrator: { delegate },
      config: createConfig({
        defaults: {
          ...createConfig().defaults,
          bridge_entry_max_chars: 5,
        },
      }),
    });

    const result = await orchestrator.run({ task: 'x', max_rounds: 2 });
    expect(result.stop_snapshot.stop_reason).toBe('bridge_validation_failed');
    expect(result.stop_snapshot.round_reached).toBe(1);
  });

  it('recovers from wrapped or mildly malformed model JSON responses', async () => {
    const delegate = vi.fn(async ({ message }: { message: string }) => {
      const payload = JSON.parse(message);
      if (payload.brief_D && payload.brief_P) {
        return {
          content: [
            '```json',
            '{result: {schema_version: \'1.0.0\', selected_primary: [\'D.r1.01\'], selected_secondary: [\'P.r1.01\'], merges: [], rejections: [], open_questions: [\'q1\'], next_experiments: [\'e1\']}}',
            '```',
          ].join('\n'),
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }
      if (payload.shortlisted_ideas) {
        return {
          content: '{"output":{"grounded":[{"idea_id":"D.r1.01","mve":"m","constraints":["c"],"falsifiability_checks":["f"]}]}}',
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }
      if (payload.ideas) {
        return {
          content: [
            'Assessment follows.',
            '```json',
            '{data: {assessments: [{idea_id: "D.r1.01", scores: {novelty: 60, feasibility: 70, impact: 80, testability: 90}, decision: "shortlist", notes: "ok"}], assumptions: ["a"], risks: ["r"], asks: ["k"], what_to_steal: ["w"], convergence_signal: false, novelty_score: 60, repetition_rate: 20}}',
            '```',
          ].join('\n'),
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }
      if (payload.group && payload.round) {
        return {
          content: [
            'Here is the draft:',
            '```json',
            '{output: {ideas: [{title: \'t1\', hypothesis: \'h1\', mechanism: \'m1\', expected_outcome: \'o1\'}]}}',
            '```',
          ].join('\n'),
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }
      throw new Error('Unexpected payload');
    });

    const orchestrator = new CouncilsOrchestrator({
      registry: createRegistry(),
      orchestrator: { delegate },
      config: createConfig(),
    });

    const result = await orchestrator.run({ task: 'x', max_rounds: 1 });
    expect(result.stop_snapshot.round_reached).toBe(1);
    expect(result.meta.selected_primary[0]).toBe('D.r1.01');
    expect(result.meta.selected_secondary[0]).toBe('P.r1.01');
  });

  it('retries once with agent-assisted JSON repair when initial output is invalid', async () => {
    const repairPayloads: Array<Record<string, unknown>> = [];
    const seenIdeation = new Set<string>();

    const delegate = vi.fn(async ({ message }: { message: string }) => {
      const payload = JSON.parse(message) as Record<string, any>;

      if (payload.task === 'normalize_json_output') {
        repairPayloads.push(payload);
        const schema = String(payload.required_schema ?? '');
        if (schema.includes('"ideas"')) {
          return {
            content: JSON.stringify({
              ideas: [
                {
                  title: 'repaired idea',
                  hypothesis: 'h',
                  mechanism: 'm',
                  expected_outcome: 'o',
                },
              ],
            }),
            usage: { inputTokens: 10, outputTokens: 5 },
            tier: 'default' as const,
          };
        }
        throw new Error('Unexpected repair schema');
      }

      if (payload.brief_D && payload.brief_P) {
        return {
          content: JSON.stringify({
            schema_version: '1.0.0',
            selected_primary: [payload.brief_D.shortlist[0]],
            selected_secondary: [payload.brief_P.shortlist[0]],
            merges: [],
            rejections: [],
            open_questions: ['q1'],
            next_experiments: ['e1'],
          }),
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }
      if (payload.shortlisted_ideas) {
        return {
          content: JSON.stringify({
            grounded: payload.shortlisted_ideas.map((idea: any) => ({
              idea_id: idea.idea_id,
              mve: 'm',
              constraints: ['c'],
              falsifiability_checks: ['f'],
            })),
          }),
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }
      if (payload.ideas) {
        return {
          content: JSON.stringify({
            assessments: payload.ideas.map((idea: any, idx: number) => ({
              idea_id: idea.idea_id,
              scores: { novelty: 50, feasibility: 50, impact: 50, testability: 50 },
              decision: idx === 0 ? 'shortlist' : 'hold',
              notes: 'ok',
            })),
            assumptions: [],
            risks: [],
            asks: [],
            what_to_steal: [],
            convergence_signal: false,
            novelty_score: 60,
            repetition_rate: 20,
          }),
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }
      if (payload.group && payload.round) {
        const key = `${payload.group}:${payload.round}`;
        if (!seenIdeation.has(key)) {
          seenIdeation.add(key);
          return {
            content: 'this is not JSON',
            usage: { inputTokens: 10, outputTokens: 5 },
            tier: 'default' as const,
          };
        }
        return {
          content: JSON.stringify({
            ideas: [
              {
                title: `${payload.group} idea`,
                hypothesis: 'h',
                mechanism: 'm',
                expected_outcome: 'o',
              },
            ],
          }),
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }

      throw new Error('Unexpected payload');
    });

    const orchestrator = new CouncilsOrchestrator({
      registry: createRegistry(),
      orchestrator: { delegate },
      config: createConfig({ defaults: { ...createConfig().defaults, max_rounds: 1 } }),
    });

    const result = await orchestrator.run({ task: 'x', max_rounds: 1 });
    expect(result.stop_snapshot.round_reached).toBe(1);
    expect(repairPayloads.length).toBeGreaterThan(0);
  });

  it('falls back instead of throwing when repair output is still invalid', async () => {
    const delegate = vi.fn(async ({ message }: { message: string }) => {
      const payload = JSON.parse(message) as Record<string, any>;

      if (payload.task === 'normalize_json_output') {
        return {
          content: 'still invalid json',
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }

      if (payload.brief_D && payload.brief_P) {
        return {
          content: JSON.stringify({
            schema_version: '1.0.0',
            selected_primary: [payload.brief_D.shortlist[0]],
            selected_secondary: [payload.brief_P.shortlist[0]],
            merges: [],
            rejections: [],
            open_questions: ['q1'],
            next_experiments: ['e1'],
          }),
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }

      if (payload.shortlisted_ideas) {
        return {
          content: JSON.stringify({ grounded: [] }),
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }

      if (payload.ideas) {
        return {
          content: JSON.stringify({
            assessments: payload.ideas.map((idea: any, idx: number) => ({
              idea_id: idea.idea_id,
              scores: { novelty: 50, feasibility: 50, impact: 50, testability: 50 },
              decision: idx === 0 ? 'shortlist' : 'hold',
              notes: 'ok',
            })),
            assumptions: [],
            risks: [],
            asks: [],
            what_to_steal: [],
            convergence_signal: false,
            novelty_score: 60,
            repetition_rate: 20,
          }),
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }

      if (payload.group && payload.round) {
        return {
          content: 'not json at all',
          usage: { inputTokens: 10, outputTokens: 5 },
          tier: 'default' as const,
        };
      }

      throw new Error('Unexpected payload');
    });

    const orchestrator = new CouncilsOrchestrator({
      registry: createRegistry(),
      orchestrator: { delegate },
      config: createConfig({ defaults: { ...createConfig().defaults, max_rounds: 1 } }),
    });

    const result = await orchestrator.run({ task: 'x', max_rounds: 1 });
    expect(result.stop_snapshot.round_reached).toBe(1);
    expect(result.brief_D_v1.ideas.length).toBeGreaterThan(0);
    expect(result.brief_P_v1.ideas.length).toBeGreaterThan(0);
    expect(result.trace.some((event) => event.validation_failure === 'repair_failed')).toBe(true);
  });
});
