/**
 * Spike M9: Termination Oracle Validation
 *
 * Comprehensive test suite validating that ALL kernel termination paths
 * route through the single-owner arbitrator gateway per the Stage 5 W4
 * architectural fix (terminate.ts).
 *
 * Failure modes validated:
 * - FM-D1: Verify no 9-path scatter — all status:"done" transitions occur via terminate()
 * - False positive: Arbitrator doesn't prematurely extend runs
 * - False negative: Arbitrator doesn't falsely clip valid runs
 */

import { describe, it, expect, beforeEach } from "bun:test";
import { resolve } from "node:path";
import type { KernelState, KernelInput, KernelContext } from "../src/kernel/state/kernel-state.js";
import { initialKernelState, transitionState } from "../src/kernel/state/kernel-state.js";
import { terminate } from "../src/kernel/loop/terminate.js";
import { makeStep } from "../src/kernel/capabilities/sense/step-utils.js";
import { modelSynthesisDeliverable } from "@reactive-agents/core";

/**
 * Instrumentation layer: track all state transitions to verify termination
 * only occurs through the terminate() helper or the Arbitrator.
 */
interface TerminationEvent {
  readonly iteration: number;
  readonly reason?: string;
  readonly terminatedBy?: string;
  readonly via: "terminate_helper" | "transitionState_direct";
  readonly output?: string;
}

let terminationLog: TerminationEvent[] = [];

// Patch transitionState to detect direct status:"done" assignments
const originalTransitionState = transitionState;
function patchedTransitionState(state: KernelState, update: any): KernelState {
  if (update.status === "done" && state.status !== "done") {
    // Detect if this is a direct transition vs via terminate()
    // We'll check the call stack (fragile but useful for testing)
    const stackStr = new Error().stack ?? "";
    const isViaTerminate = stackStr.includes("terminate");
    const isViaArbitrator = stackStr.includes("arbitrate");

    if (!isViaTerminate && !isViaArbitrator) {
      terminationLog.push({
        iteration: state.iteration,
        terminatedBy: update.meta?.terminatedBy,
        via: "transitionState_direct",
        output: update.output,
      });
    }
  }
  return originalTransitionState(state, update);
}

describe("M9 — Termination Oracle Validation", () => {
  beforeEach(() => {
    terminationLog = [];
  });

  // ────────────────────────────────────────────────────────────────────────
  // SECTION 1: Verify terminate() Helper Mechanics
  // ────────────────────────────────────────────────────────────────────────

  describe("1. terminate() helper mechanics", () => {
    it("should set status:'done' with terminatedBy reason", () => {
      const state = initialKernelState({ maxIterations: 10, strategy: "reactive", kernelType: "default" });

      const terminated = terminate(state, {
        reason: "low_delta_guard",
        deliverable: modelSynthesisDeliverable({ type: "thought", content: "Task complete.", iteration: 0 }),
      });

      expect(terminated.status).toBe("done");
      expect(terminated.meta.terminatedBy).toBe("low_delta_guard");
      expect(terminated.output).toBe("Task complete.");
    });

    it("should merge extraMeta alongside terminatedBy", () => {
      const state = initialKernelState({ maxIterations: 10, strategy: "reactive", kernelType: "default" });

      const terminated = terminate(state, {
        reason: "oracle_forced",
        deliverable: modelSynthesisDeliverable({ type: "thought", content: "Forced exit.", iteration: 0 }),
        extraMeta: { nudgeCount: 2, escalateTo: "user_review" } as any,
      });

      expect(terminated.meta.terminatedBy).toBe("oracle_forced");
      expect((terminated.meta as any).nudgeCount).toBe(2);
      expect((terminated.meta as any).escalateTo).toBe("user_review");
    });

    it("should preserve previous terminatedBy in extraMeta if caller provides it", () => {
      const state = initialKernelState({ maxIterations: 10, strategy: "reactive", kernelType: "default" });

      const terminated = terminate(state, {
        reason: "harness_deliverable",
        deliverable: modelSynthesisDeliverable({ type: "thought", content: "Assembled from artifacts.", iteration: 0 }),
        extraMeta: { previousTerminatedBy: "some_prior_reason" } as any,
      });

      expect(terminated.meta.terminatedBy).toBe("harness_deliverable");
      expect((terminated.meta as any).previousTerminatedBy).toBe("some_prior_reason");
    });

    it("should allow empty output (per output-boundary discipline)", () => {
      const state = initialKernelState({ maxIterations: 10, strategy: "reactive", kernelType: "default" });

      const terminated = terminate(state, {
        reason: "fallback_deliver",
        deliverable: modelSynthesisDeliverable({ type: "thought", content: "", iteration: 0 }),
      });

      expect(terminated.output).toBe("");
      expect(terminated.status).toBe("done");
    });
  });

  // ────────────────────────────────────────────────────────────────────────
  // SECTION 2: Termination Reasons Coverage (from runner.ts grep)
  // ────────────────────────────────────────────────────────────────────────

  describe("2. Termination reasons coverage (9 paths)", () => {
    const reasons = [
      "low_delta_guard",
      "harness_deliverable",
      "oracle_forced",
      "switching_exhausted",
      "loop_graceful",
    ];

    reasons.forEach((reason) => {
      it(`should handle termination reason: ${reason}`, () => {
        const state = initialKernelState({
          strategy: "reactive",
          taskId: "test",
          maxIterations: 10,
          kernelType: "default",
        });

        const terminated = terminate(state, {
          reason,
          deliverable: modelSynthesisDeliverable({ type: "thought", content: `Terminated by ${reason}`, iteration: 0 }),
        });

        expect(terminated.status).toBe("done");
        expect(terminated.meta.terminatedBy).toBe(reason);
      });
    });
  });

  // ────────────────────────────────────────────────────────────────────────
  // SECTION 3: No False Positives (Premature Extension)
  // ────────────────────────────────────────────────────────────────────────

  describe("3. No false positives (premature extension)", () => {
    it("should not extend a run that already has status:'done'", () => {
      let state = initialKernelState({
        strategy: "reactive",
        taskId: "test",
        maxIterations: 10,
        kernelType: "default",
      });

      // First termination
      state = terminate(state, {
        reason: "low_delta_guard",
        deliverable: modelSynthesisDeliverable({ type: "thought", content: "First termination.", iteration: 0 }),
      });

      expect(state.status).toBe("done");
      expect(state.meta.terminatedBy).toBe("low_delta_guard");

      // Attempt second termination (simulate buggy code trying to re-terminate)
      state = terminate(state, {
        reason: "oracle_forced",
        deliverable: modelSynthesisDeliverable({ type: "thought", content: "Second termination.", iteration: 0 }),
      });

      // Should transition but preserving immutability
      expect(state.status).toBe("done");
      // Latest terminatedBy wins (per transitionState semantics)
      expect(state.meta.terminatedBy).toBe("oracle_forced");
    });

    it("should enforce that only the kernel loop can direct status transitions", () => {
      // This test documents the design: transitionState allows arbitrary
      // status changes (it's a low-level primitive). The kernel loop is
      // responsible for ensuring no backwards transitions occur.
      // The arbitrator and terminate() are the safe APIs that enforce forward-only.

      let state = initialKernelState({
        strategy: "reactive",
        taskId: "test",
        maxIterations: 10,
        kernelType: "default",
      });

      state = terminate(state, {
        reason: "low_delta_guard",
        deliverable: modelSynthesisDeliverable({ type: "thought", content: "Done.", iteration: 0 }),
      });

      expect(state.status).toBe("done");

      // transitionState is a low-level primitive that allows any change
      // but the kernel loop's main while condition should never attempt this
      const arbitraryChange = transitionState(state, {
        status: "thinking",
      });

      // The raw transitionState DOES allow this (it's immutable + primitive),
      // but the kernel loop enforces the invariant by breaking on status:"done"
      expect(arbitraryChange.status).toBe("thinking");
    });
  });

  // ────────────────────────────────────────────────────────────────────────
  // SECTION 4: No False Negatives (Premature Termination)
  // ────────────────────────────────────────────────────────────────────────

  describe("4. No false negatives (premature termination)", () => {
    it("should not terminate when missing required tools", () => {
      const state = initialKernelState({
        strategy: "reactive",
        taskId: "test",
        steps: [makeStep("thought", "I need to call search_web")],
        requiredTools: ["search_web"],
        toolsUsed: new Set(),
      });

      // No terminate() call here — the harness should NOT exit yet
      expect(state.status).toBe("thinking");
      expect(state.toolsUsed.size).toBe(0);

      // simulate kernel continuing (no termination)
      const continued = transitionState(state, {
        steps: [
          ...state.steps,
          makeStep("action", "Calling search_web", {
            toolUsed: "search_web",
          }),
        ],
      });

      expect(continued.status).toBe("thinking");
    });

    it("should terminate only after required tools are satisfied", () => {
      let state = initialKernelState({
        strategy: "reactive",
        taskId: "test",
        steps: [makeStep("thought", "I need to call search_web")],
        requiredTools: ["search_web"],
        toolsUsed: new Set(),
      });

      // Add action and observation for required tool
      state = transitionState(state, {
        steps: [
          ...state.steps,
          makeStep("action", "Calling search_web", {
            toolUsed: "search_web",
          }),
          makeStep("observation", "Found result"),
        ],
        toolsUsed: new Set(["search_web"]),
      });

      // Now can terminate
      state = terminate(state, {
        reason: "oracle_forced",
        deliverable: modelSynthesisDeliverable({ type: "thought", content: "Task complete with required tool called.", iteration: 0 }),
      });

      expect(state.status).toBe("done");
      expect(state.toolsUsed.has("search_web")).toBe(true);
    });
  });

  // ────────────────────────────────────────────────────────────────────────
  // SECTION 5: Regression Gate (Behavioral No-Change)
  // ────────────────────────────────────────────────────────────────────────

  describe("5. Regression gate (behavioral no-change)", () => {
    it("should preserve output exactly as provided", () => {
      const outputs = [
        "Simple answer",
        "Multi-line\noutput\nhere",
        "With special chars: !@#$%^&*()",
        "",
      ];

      for (const output of outputs) {
        const state = initialKernelState({
          strategy: "reactive",
          taskId: "test",
          });

        const terminated = terminate(state, {
          reason: "low_delta_guard",
          deliverable: modelSynthesisDeliverable({ type: "thought", content: output, iteration: 0 }),
        });

        expect(terminated.output).toBe(output);
      }
    });

    it("should preserve iteration count unchanged", () => {
      const state = initialKernelState({ maxIterations: 10, strategy: "reactive", kernelType: "default" });

      // Simulate iterations
      let iterState = transitionState(state, { iteration: 5 });

      const terminated = terminate(iterState, {
        reason: "low_delta_guard",
        deliverable: modelSynthesisDeliverable({ type: "thought", content: "Done after 5 iterations.", iteration: 0 }),
      });

      expect(terminated.iteration).toBe(5);
    });

    it("should preserve steps unchanged", () => {
      const step1 = makeStep("thought", "Thinking...");
      const step2 = makeStep("action", "Calling tool", { toolUsed: "search" });
      const step3 = makeStep("observation", "Got result");

      let state = initialKernelState({
        strategy: "reactive",
        taskId: "test",
        maxIterations: 10,
        kernelType: "default",
      });

      // Manually build up state with steps
      state = transitionState(state, {
        steps: [step1, step2, step3],
      });

      const terminated = terminate(state, {
        reason: "oracle_forced",
        deliverable: modelSynthesisDeliverable({ type: "thought", content: "Done.", iteration: 0 }),
      });

      expect(terminated.steps.length).toBe(3);
      expect(terminated.steps[0]?.content).toBe("Thinking...");
      expect(terminated.steps[1]?.content).toBe("Calling tool");
      expect(terminated.steps[2]?.content).toBe("Got result");
    });

    it("should preserve toolsUsed set unchanged", () => {
      let state = initialKernelState({
        strategy: "reactive",
        taskId: "test",
        maxIterations: 10,
        kernelType: "default",
      });

      // Build up toolsUsed via transition
      state = transitionState(state, {
        toolsUsed: new Set(["search_web", "get_weather", "calculate"]),
      });

      const terminated = terminate(state, {
        reason: "harness_deliverable",
        deliverable: modelSynthesisDeliverable({ type: "thought", content: "Assembled result.", iteration: 0 }),
      });

      expect(terminated.toolsUsed.has("search_web")).toBe(true);
      expect(terminated.toolsUsed.has("get_weather")).toBe(true);
      expect(terminated.toolsUsed.has("calculate")).toBe(true);
      expect(terminated.toolsUsed.size).toBe(3);
    });
  });

  // ────────────────────────────────────────────────────────────────────────
  // SECTION 6: Arbitrator Integration Validation
  // ────────────────────────────────────────────────────────────────────────

  describe("6. Arbitrator integration (sole termination oracle)", () => {
    it("should support arbitrator's own transitionState call (allowed bypass)", () => {
      // The arbitrator is allowed to call transitionState directly for its own
      // verdict-driven exit-success branch (per terminate.ts line 48-49).
      // This test documents that this is intentional and acceptable.

      const state = initialKernelState({
        strategy: "reactive",
        taskId: "test",
        steps: [makeStep("thought", "Task completed successfully.")],
      });

      // Simulate arbitrator's direct verdict application
      const arbitratorTerminated = transitionState(state, {
        status: "done",
        output: "Verdict-driven exit.",
        meta: {
          ...state.meta,
          terminatedBy: "arbitrator_verdict",
        },
      });

      expect(arbitratorTerminated.status).toBe("done");
      expect(arbitratorTerminated.meta.terminatedBy).toBe("arbitrator_verdict");
    });

    it("should track that dispatcher-early-stop flows through arbitrator", () => {
      // Per runner.ts:720-733, dispatcher-early-stop is intentionally
      // routed through arbitrateAndApply so the veto can override.

      const state = initialKernelState({
        strategy: "reactive",
        taskId: "test",
        meta: {
          taskId: "test",
          maxIterations: 10,
          terminatedBy: "dispatcher-early-stop",
        },
      });

      // The fact that terminatedBy is set means the runner detected the
      // dispatcher signal and will call arbitrateAndApply (not shown here
      // because arbitrator is in a different package).
      expect(state.meta.terminatedBy).toBe("dispatcher-early-stop");
    });

    it("enforces the single-owner termination invariant via the lint script (no stray status:'done')", () => {
      // REAL guard (was a tautology asserting an array contained its own
      // literal). The single-owner invariant is enforced by
      // scripts/check-termination-paths.sh, which fails if any direct
      // `status:"done"` transition appears in packages/reasoning/src outside the
      // two authorized owners (terminate.ts / arbitrator.ts). Run it for real
      // and assert it passes — a new scattered termination site turns this RED.
      const repoRoot = resolve(import.meta.dir, "..", "..", "..");
      const proc = Bun.spawnSync(
        ["bash", "scripts/check-termination-paths.sh"],
        { cwd: repoRoot, stdout: "pipe", stderr: "pipe" },
      );
      const out = proc.stdout.toString() + proc.stderr.toString();
      // Surface the offending lines if the invariant is violated.
      expect(out).toContain("Termination invariant holds");
      expect(proc.exitCode).toBe(0);
    });
  });

  // ────────────────────────────────────────────────────────────────────────
  // SECTION 7: Immutability and Determinism
  // ────────────────────────────────────────────────────────────────────────

  describe("7. Immutability and determinism", () => {
    it("should not mutate original state", () => {
      const state = initialKernelState({ maxIterations: 10, strategy: "reactive", kernelType: "default" });

      const originalStatus = state.status;
      const originalMeta = { ...state.meta };

      const _terminated = terminate(state, {
        reason: "low_delta_guard",
        deliverable: modelSynthesisDeliverable({ type: "thought", content: "Done.", iteration: 0 }),
      });

      // Original should be unchanged
      expect(state.status).toBe(originalStatus);
      expect(state.meta.terminatedBy).toBe(originalMeta.terminatedBy);
    });

    it("should be deterministic (same input → same output)", () => {
      const state = initialKernelState({
        strategy: "reactive",
        taskId: "test",
        iteration: 5,
      });

      const opts = {
        reason: "oracle_forced" as const,
        deliverable: modelSynthesisDeliverable({ type: "thought", content: "Deterministic output", iteration: 0 }),
        extraMeta: {} as any,
      };

      const result1 = terminate(state, opts);
      const result2 = terminate(state, opts);

      expect(result1.status).toBe(result2.status);
      expect(result1.meta.terminatedBy).toBe(result2.meta.terminatedBy);
      expect(result1.output).toBe(result2.output);
      expect((result1.meta as any).nudgeCount).toBe(
        (result2.meta as any).nudgeCount
      );
    });
  });

  // ────────────────────────────────────────────────────────────────────────
  // SECTION 8: Documentation Compliance
  // ────────────────────────────────────────────────────────────────────────

  describe("8. Documentation compliance", () => {
    it("should fulfill terminate.ts design contract", () => {
      // From terminate.ts line 45-60:
      // Every termination must declare why (terminatedBy reason).
      // The reason must be non-empty.

      const state = initialKernelState({ maxIterations: 10, strategy: "reactive", kernelType: "default" });

      const terminated = terminate(state, {
        reason: "harness_deliverable",
        deliverable: modelSynthesisDeliverable({ type: "thought", content: "Assembled result", iteration: 0 }),
      });

      expect(terminated.meta.terminatedBy).toBeTruthy();
      expect(typeof terminated.meta.terminatedBy).toBe("string");
      expect(terminated.meta.terminatedBy.length).toBeGreaterThan(0);
    });

    it("should support all documented termination reasons", () => {
      // From terminate.ts comment (line 27): Common values listed in JSDoc.
      const documentedReasons = [
        "low_delta_guard",
        "harness_deliverable",
        "oracle_forced",
        "loop_graceful",
        "dispatcher-early-stop",
        "dispatcher-strategy-switch",
      ];

      for (const reason of documentedReasons) {
        const state = initialKernelState({
          strategy: "reactive",
          taskId: "test",
          });

        const terminated = terminate(state, {
          reason,
          deliverable: modelSynthesisDeliverable({ type: "thought", content: "Done", iteration: 0 }),
        });

        expect(terminated.meta.terminatedBy).toBe(reason);
      }
    });
  });
});
