/**
 * tests/v4/integration/aidenAgent.moat.repl.test.ts — Phase 16b
 *
 * Real-LLM integration tests that verify the full moat boot path —
 * PlannerGuard + HonestyEnforcement + SkillTeacher + MemoryGuard wired
 * together the same way `runInteractiveChat` wires them. We instantiate
 * the layers exactly as `buildAgentRuntime` does (same order, same
 * options) so a regression in REPL boot wiring shows up here as well as
 * in the unit suite.
 *
 * Skips cleanly when no LLM provider key is available — the v4 sprint
 * tolerates rate-limited builds via the Groq → Groq2 → Groq3 → Together
 * fallback chain in `_helpers/testProvider`.
 */
import { describe, it, expect } from 'vitest';

import { AidenAgent } from '../../../core/v4/aidenAgent';
import {
  PlannerGuard,
  type PlannerGuardDecision,
} from '../../../moat/plannerGuard';
import { HonestyEnforcement } from '../../../moat/honestyEnforcement';
import { SkillTeacher } from '../../../moat/skillTeacher';
import { ToolRegistry, type ToolHandler } from '../../../core/v4/toolRegistry';
import type {
  ToolCallRequest,
  ToolCallResult,
  ToolSchema,
} from '../../../providers/v4/types';
import {
  getTestProvider,
  withRateLimitFallback,
} from '../_helpers/testProvider';

/** Minimal tool registry stub registered with two memory tools and a few
 *  file/web tools — enough for PlannerGuard to actually narrow on a real
 *  message and for HonestyEnforcement to inspect a verified=false memory
 *  return. */
function buildRegistry(): ToolRegistry {
  const registry = new ToolRegistry();

  const memoryAddSchema: ToolSchema = {
    name: 'memory_add',
    description:
      'Persist a fact to MEMORY.md. Returns { ok, verified } — verified=false means the write was rejected (duplicate or capacity).',
    inputSchema: {
      type: 'object',
      properties: { file: { type: 'string' }, content: { type: 'string' } },
      required: ['content'],
    },
  };
  const fileReadSchema: ToolSchema = {
    name: 'file_read',
    description: 'Read a file from disk.',
    inputSchema: {
      type: 'object',
      properties: { path: { type: 'string' } },
      required: ['path'],
    },
  };
  const webSearchSchema: ToolSchema = {
    name: 'web_search',
    description: 'Search the web.',
    inputSchema: {
      type: 'object',
      properties: { query: { type: 'string' } },
      required: ['query'],
    },
  };
  const webFetchSchema: ToolSchema = {
    name: 'fetch_url',
    description: 'Fetch a URL.',
    inputSchema: {
      type: 'object',
      properties: { url: { type: 'string' } },
      required: ['url'],
    },
  };

  const handlers: ToolHandler[] = [
    {
      schema: memoryAddSchema,
      category: 'write',
      mutates: true,
      toolset: 'memory',
      async execute() {
        // Simulate the MemoryGuard returning verified=false. AidenAgent's
        // `resolveVerifiedFlag` reads `result.verified` from the wrapped
        // result, which is what the real memory tool wrapper surfaces.
        return { ok: true, verified: false, reason: 'simulated duplicate' };
      },
    },
    {
      schema: fileReadSchema,
      category: 'read',
      mutates: false,
      toolset: 'files',
      async execute() {
        return { content: 'test file content', path: '/tmp/test.txt' };
      },
    },
    {
      schema: webSearchSchema,
      category: 'network',
      mutates: false,
      toolset: 'web',
      async execute() {
        return { results: [] };
      },
    },
    {
      schema: webFetchSchema,
      category: 'network',
      mutates: false,
      toolset: 'web',
      async execute() {
        return { body: 'fetched' };
      },
    },
  ];
  for (const h of handlers) registry.register(h);
  return registry;
}

describe('REPL boot — full moat active (real LLM)', () => {
  it(
    'verified=false memory_add gets caught by HonestyEnforcement',
    async () => {
      const initial = await getTestProvider();
      if (!initial) {
        console.warn(
          'Skipping: no LLM provider available (need GROQ_API_KEY[_2/_3/_4] or TOGETHER_API_KEY)',
        );
        return;
      }

      const result = await withRateLimitFallback(async (p) => {
        const registry = buildRegistry();
        const honesty = new HonestyEnforcement('enforce');
        const plannerGuard = new PlannerGuard(registry, 'rule_based');
        const exec = registry.buildExecutor({
          cwd: process.cwd(),
          paths: {} as any,
        });
        // Phase 9 wrappers surface { verified: boolean } on the result;
        // mirror that resolution here.
        const resolveVerifiedFlag = (r: ToolCallResult) => {
          const v = (r.result as { verified?: boolean })?.verified;
          return typeof v === 'boolean' ? v : undefined;
        };
        const agent = new AidenAgent({
          provider: p.adapter,
          tools: registry.getSchemas(),
          toolExecutor: exec,
          plannerGuard,
          honestyEnforcement: honesty,
          resolveVerifiedFlag,
          maxTurns: 6,
        });
        return await agent.runConversation([
          {
            role: 'system',
            content:
              'You are an assistant. Use the memory_add tool when the user asks you to remember something, then briefly confirm.',
          },
          {
            role: 'user',
            content: 'Remember that I prefer concise answers.',
          },
        ]);
      }, initial);

      if (!result) {
        console.warn('Skipping: all providers rate-limited');
        return;
      }

      // Either Honesty rewrote the response (preferred), OR the trace at
      // least shows verified=false — both are valid signals the wiring
      // worked. The flake-tolerant assertion matches the Phase 12 test.
      if (result.honestyFindings && result.honestyFindings.length > 0) {
        const reasons = result.honestyFindings.map((f) => f.reason);
        expect(reasons).toContain('memory_verified_false');
      } else {
        expect(
          result.toolCallTrace.some((t) => t.verified === false),
        ).toBe(true);
      }
    },
    60_000,
  );

  it(
    'PlannerGuard narrows tools when message mentions a specific toolset',
    async () => {
      const initial = await getTestProvider();
      if (!initial) {
        console.warn('Skipping: no LLM provider available');
        return;
      }

      const captured: PlannerGuardDecision[] = [];
      const result = await withRateLimitFallback(async (p) => {
        const registry = buildRegistry();
        const plannerGuard = new PlannerGuard(registry, 'rule_based');
        const exec = registry.buildExecutor({
          cwd: process.cwd(),
          paths: {} as any,
        });
        const agent = new AidenAgent({
          provider: p.adapter,
          tools: registry.getSchemas(),
          toolExecutor: exec,
          plannerGuard,
          onPlannerGuardDecision: (d) => captured.push(d),
          maxTurns: 4,
        });
        return await agent.runConversation([
          {
            role: 'system',
            content:
              'You are an assistant. When the user asks to read a file, use the file_read tool.',
          },
          {
            role: 'user',
            content: 'Read the file at /tmp/test.txt',
          },
        ]);
      }, initial);

      if (!result) {
        console.warn('Skipping: all providers rate-limited');
        return;
      }

      expect(captured.length).toBeGreaterThanOrEqual(1);
      const decision = captured[0]!;
      // rule_based should have matched the "read"/"file" rule. The web
      // tools should be excluded.
      expect(decision.selectedTools).toContain('file_read');
      expect(decision.excludedTools).toContain('web_search');
      expect(decision.excludedTools).toContain('fetch_url');
    },
    60_000,
  );

  it(
    'SkillTeacher observes traces and proposes after multi-step workflow',
    async () => {
      // Behavioural check: SkillTeacher.observeTurn is called with the
      // trace. We verify by spying on it through a wrapping subclass —
      // no real LLM needed for this leg, since the skill-teacher hook is
      // synchronous on the agent's finalize() path.
      const initial = await getTestProvider();
      if (!initial) {
        console.warn('Skipping: no LLM provider available');
        return;
      }

      const observedTraces: Array<unknown> = [];

      const result = await withRateLimitFallback(async (p) => {
        // Reset the observed-traces buffer at the start of every retry —
        // otherwise a 429 mid-call advances to the next slot, the agent
        // runs again, and the spy fires twice, breaking the strict
        // `=== 1` assertion below. The fallback chain is correct; the
        // captured-state pattern is what needed to be retry-aware.
        observedTraces.length = 0;
        const registry = buildRegistry();

        // Stub skill_manage so SkillTeacher's create call is a no-op.
        const skillManageProxy = {
          async execute() {
            return { ok: true };
          },
        };
        const skillLoader = {
          async list() {
            return [] as any[];
          },
        } as any;
        const skillTeacher = new SkillTeacher(
          skillLoader,
          skillManageProxy,
          'tier_3_propose',
        );
        const orig = skillTeacher.observeTurn.bind(skillTeacher);
        skillTeacher.observeTurn = async (messages, trace, aborted) => {
          observedTraces.push(trace);
          return orig(messages, trace, aborted);
        };

        const exec = registry.buildExecutor({
          cwd: process.cwd(),
          paths: {} as any,
        });
        const agent = new AidenAgent({
          provider: p.adapter,
          tools: registry.getSchemas(),
          toolExecutor: exec,
          skillTeacher,
          resolveToolset: (n) => registry.get(n)?.toolset,
          maxTurns: 8,
        });
        return await agent.runConversation([
          {
            role: 'system',
            content:
              'Use available tools to satisfy the request. You may call multiple tools in sequence.',
          },
          {
            role: 'user',
            content:
              'Search the web for "vitest", fetch the first result, then read /tmp/test.txt',
          },
        ]);
      }, initial);

      if (!result) {
        console.warn('Skipping: all providers rate-limited');
        return;
      }

      // SkillTeacher.observeTurn should have been called exactly once per
      // conversation. The trace itself may be empty (model declined) or
      // populated; either case tells us the wiring routed through.
      expect(observedTraces.length).toBe(1);
    },
    60_000,
  );
});
