import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import { HeronMCPServer, type ReportDiffer } from '../../src/server/mcp-server.js';
import type { ProgressNotification, RequestContext } from '../../src/server/mcp-types.js';
import {
  createSession,
  getSession,
  setPendingQuestion,
  updateSessionMeta,
} from '../../src/storage/sessions.js';

/**
 * Unit tests for the AAP-55 submit_answer MCP tool.
 *
 * submit_answer is the second half of the tool-call interview path.
 * Lifecycle:
 *   1. start_audit_session sees a non-sampling client, creates a
 *      session with mode='tool-call', writes the first question to
 *      session.pendingQuestion, returns status='awaiting_answer'.
 *   2. The client calls submit_answer with the agent's answer.
 *      Handler appends to transcript, asks the planner for the next
 *      question. If one exists → write to pendingQuestion + return
 *      'awaiting_answer'. If exhausted → run analyzeAndRenderReport
 *      synchronously, write report, return 'complete'.
 *
 * These tests inject a stub planner + stub analyzer so we cover the
 * tool surface without dragging the real LLM in.
 */

const noopDiffer: ReportDiffer = { async diff() { return ''; } };

function makeCtx(): RequestContext {
  return {
    authPrincipal: null,
    sessionId: 'mcp-sess-1',
    progress: (_: ProgressNotification) => undefined,
    signal: new AbortController().signal,
  };
}

describe('HeronMCPServer.submit_answer (AAP-55)', () => {
  let tmpDir: string;
  const origEnv = process.env.HERON_SESSIONS_DIR;

  beforeEach(() => {
    tmpDir = mkdtempSync(join(tmpdir(), 'heron-aap55-submit-'));
    process.env.HERON_SESSIONS_DIR = tmpDir;
  });
  afterEach(() => {
    if (origEnv === undefined) delete process.env.HERON_SESSIONS_DIR;
    else process.env.HERON_SESSIONS_DIR = origEnv;
    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('lists submit_answer in the tool registry', () => {
    const server = new HeronMCPServer({ differ: noopDiffer });
    const defs = server.listToolDefinitions();
    expect(defs.map((d) => d.name)).toContain('submit_answer');
    const def = defs.find((d) => d.name === 'submit_answer')!;
    expect(def.description).toMatch(/submit_answer|in-progress|audit/i);
    const schemaText = JSON.stringify(def.inputSchema);
    expect(schemaText).toMatch(/session_id/);
    expect(schemaText).toMatch(/answer/);
  });

  it('advances the interview by one question on a happy-path call', async () => {
    const planner = {
      initial: vi.fn(() => ({ text: 'Q0', category: 'purpose' as const, index: 0 })),
      next: vi.fn(async () => ({ text: 'Q1', category: 'data' as const, index: 1 })),
      totalCoreQuestions: 9,
    };
    const server = new HeronMCPServer({
      differ: noopDiffer,
      questionPlanner: planner,
      analyzeAndRenderReport: async () => ({ ok: true, markdown: '# unused', json: {} }),
    });

    const { id } = await createSession({ agentName: 'fixture', mode: 'tool-call' });
    await setPendingQuestion(id, { text: 'Q0', category: 'purpose', index: 0 });

    const r = await server.invoke('submit_answer', { session_id: id, answer: 'A0' }, makeCtx());
    expect(r.ok).toBe(true);
    if (!r.ok) return;
    expect(r.value.status).toBe('awaiting_answer');
    expect(r.value.next_question).toBe('Q1');
    expect(r.value.questions_asked).toBe(1);

    const stored = await getSession(id);
    expect(stored!.transcript).toEqual([
      { category: 'purpose', question: 'Q0', answer: 'A0' },
    ]);
    expect(stored!.pendingQuestion).toEqual({ text: 'Q1', category: 'data', index: 1 });
  });

  it('returns status=analyzing and analyzes/persists in the background when the planner is exhausted (AAP-66)', async () => {
    // AAP-66 — what used to be a synchronous `status: 'complete'` reply
    // is now an `analyzing` envelope returned immediately, with analyze
    // + verdict computation running in a detached background promise.
    // The tool call no longer carries report_markdown / risk_level
    // (those are SSE-delivered now); see `submit-answer-async.test.ts`
    // for the full async semantics. This test keeps the original happy-
    // path shape locked: the analyzer still runs exactly once, the
    // session still flips to `complete`, and the verdict is still
    // persisted with `unverified` (Surface 2 hasn't run).
    const planner = {
      initial: vi.fn(() => ({ text: 'QA', category: 'purpose' as const, index: 0 })),
      next: vi.fn(async () => null),
      totalCoreQuestions: 1,
    };
    const analyze = vi.fn(async () => ({
      ok: true as const,
      markdown: '# Final Report',
      json: { overallRiskLevel: 'low' },
      riskLevel: 'low',
    }));
    const server = new HeronMCPServer({
      differ: noopDiffer,
      questionPlanner: planner,
      analyzeAndRenderReport: analyze,
    });

    const { id } = await createSession({ agentName: 'fixture', mode: 'tool-call' });
    await setPendingQuestion(id, { text: 'QA', category: 'purpose', index: 0 });

    const r = await server.invoke('submit_answer', { session_id: id, answer: 'AA' }, makeCtx());
    expect(r.ok).toBe(true);
    if (!r.ok) return;
    expect(r.value.status).toBe('analyzing');
    // AAP-66 — analysis output is no longer carried inline.
    expect(r.value.report_markdown).toBeUndefined();
    expect(r.value.risk_level).toBeUndefined();
    expect(r.value.questions_asked).toBe(1);

    // Wait for the background promise to land. Status flips to
    // 'complete' inside writeReport, but verdict fields (verificationStatus,
    // riskLevel) are persisted via the subsequent persistVerdict call,
    // so we wait for `verificationStatus` to be set — that's the signal
    // that the full background pipeline has settled.
    let stored = await getSession(id);
    const start = Date.now();
    while (stored?.verificationStatus === undefined && Date.now() - start < 2000) {
      await new Promise((r) => setTimeout(r, 5));
      stored = await getSession(id);
    }

    expect(analyze).toHaveBeenCalledTimes(1);
    expect(stored!.status).toBe('complete');
    expect(stored!.report).toBe('# Final Report');
    expect(stored!.pendingQuestion ?? null).toBeNull();
    // AAP-63 — verdict fields persisted.
    expect(stored!.verificationStatus).toBe('unverified');
    expect(stored!.riskLevel).toBe('unverified');
    // The analyzer JSON did not include `risks`, so interviewRiskLevel
    // stays undefined here.
    expect(stored!.deterministicRiskLevel).toBeUndefined();
  });

  it('errors when the session id is unknown', async () => {
    const server = new HeronMCPServer({
      differ: noopDiffer,
      questionPlanner: {
        initial: () => ({ text: 'Q', category: 'purpose', index: 0 }),
        next: async () => null,
        totalCoreQuestions: 0,
      },
      analyzeAndRenderReport: async () => ({ ok: true, markdown: '', json: {} }),
    });

    const r = await server.invoke(
      'submit_answer',
      { session_id: 'sess-20260101-000000-deadbe', answer: 'x' },
      makeCtx(),
    );
    expect(r.ok).toBe(false);
    if (r.ok) return;
    expect(r.error.kind).toBe('tool_failure');
  });

  it('rejects a session that is already complete', async () => {
    const planner = {
      initial: () => ({ text: 'Q', category: 'purpose' as const, index: 0 }),
      next: async () => null,
      totalCoreQuestions: 0,
    };
    const server = new HeronMCPServer({
      differ: noopDiffer,
      questionPlanner: planner,
      analyzeAndRenderReport: async () => ({ ok: true, markdown: '', json: {} }),
    });
    const { id } = await createSession({ agentName: 'x', mode: 'tool-call' });
    await updateSessionMeta(id, { status: 'complete' });

    const r = await server.invoke('submit_answer', { session_id: id, answer: 'x' }, makeCtx());
    expect(r.ok).toBe(false);
    if (r.ok) return;
    expect(r.error.kind).toBe('tool_failure');
  });

  it('rejects a session that is in the sampling-mode interviewing state', async () => {
    const planner = {
      initial: () => ({ text: 'Q', category: 'purpose' as const, index: 0 }),
      next: async () => null,
      totalCoreQuestions: 0,
    };
    const server = new HeronMCPServer({
      differ: noopDiffer,
      questionPlanner: planner,
      analyzeAndRenderReport: async () => ({ ok: true, markdown: '', json: {} }),
    });
    // Sampling mode session — status starts at 'interviewing'.
    const { id } = await createSession({ agentName: 'x' /* no mode */ });

    const r = await server.invoke('submit_answer', { session_id: id, answer: 'x' }, makeCtx());
    expect(r.ok).toBe(false);
    if (r.ok) return;
    expect(r.error.kind).toBe('tool_failure');
  });

  it('rejects malformed input (missing session_id, non-string answer)', async () => {
    const planner = {
      initial: () => ({ text: 'Q', category: 'purpose' as const, index: 0 }),
      next: async () => null,
      totalCoreQuestions: 0,
    };
    const server = new HeronMCPServer({
      differ: noopDiffer,
      questionPlanner: planner,
      analyzeAndRenderReport: async () => ({ ok: true, markdown: '', json: {} }),
    });

    const r1 = await server.invoke(
      'submit_answer',
      { answer: 'x' } as unknown as { session_id: string; answer: string },
      makeCtx(),
    );
    expect(r1.ok).toBe(false);
    if (r1.ok) return;
    expect(r1.error.kind).toBe('invalid_input');

    const r2 = await server.invoke(
      'submit_answer',
      { session_id: 'sess-20260101-000000-abc123', answer: 42 as unknown as string },
      makeCtx(),
    );
    expect(r2.ok).toBe(false);
    if (r2.ok) return;
    expect(r2.error.kind).toBe('invalid_input');
  });
});
