import { describe, it, expect } from "bun:test";
import { Effect } from "effect";
import { executeReActKernel, reactKernel, detectCompletionGaps } from "../../../src/kernel/loop/react-kernel.js";
import { TestLLMServiceLayer } from "@reactive-agents/llm-provider";
import {
  initialKernelState,
  transitionState,
  noopHooks,
  type KernelContext,
  type KernelState,
} from "../../../src/kernel/state/kernel-state.js";
import { CONTEXT_PROFILES } from "../../../src/context/context-profile.js";
import { createToolCallResolver, TextParseDriver } from "@reactive-agents/tools";

describe("executeReActKernel", () => {
  it("produces a final answer for a simple task (no tools)", async () => {
    const layer = TestLLMServiceLayer([
      { match: "Task:", text: "FINAL ANSWER: The answer is 42." },
    ]);
    const result = await Effect.runPromise(
      executeReActKernel({
        task: "What is 6 times 7?",
        maxIterations: 3,
      }).pipe(Effect.provide(layer)),
    );
    expect(result.output).toBe("The answer is 42.");
    expect(result.terminatedBy).toBe("final_answer");
    expect(result.iterations).toBe(1);
  });

  it("terminates via content stability when repeated thoughts detected", async () => {
    const layer = TestLLMServiceLayer([
      { match: "Task:", text: "I need to think more about this complex problem." },
    ]);
    const result = await Effect.runPromise(
      executeReActKernel({
        task: "Solve an extremely hard problem",
        maxIterations: 2,
      }).pipe(Effect.provide(layer)),
    );
    // LLMEndTurn evaluator (no iteration/length guards) exits on the first
    // non-empty end_turn response when there are no required tools remaining.
    expect(result.terminatedBy).toBe("end_turn");
    expect(result.iterations).toBe(1);
    expect(result.steps.length).toBe(1);
  });

  it("injects priorContext into the thought prompt", async () => {
    // Match text that appears only when priorContext is merged into the system prompt.
    // (A plain "last turn repeats" scenario would false-pass if matching failed.)
    const layer = TestLLMServiceLayer([
      { match: "add more concrete examples", text: "FINAL ANSWER: Improved response incorporating the critique feedback." },
    ]);
    const result = await Effect.runPromise(
      executeReActKernel({
        task: "Explain quantum computing",
        priorContext: "A previous critique says: add more concrete examples",
        maxIterations: 2,
      }).pipe(Effect.provide(layer)),
    );
    expect(result.output).toContain("Improved response");
    expect(result.terminatedBy).toBe("final_answer");
  });

  it("records steps for each iteration", async () => {
    const layer = TestLLMServiceLayer([
      { match: "Task:", text: "FINAL ANSWER: Done." },
    ]);
    const result = await Effect.runPromise(
      executeReActKernel({ task: "Simple task", maxIterations: 3 }).pipe(
        Effect.provide(layer),
      ),
    );
    expect(result.steps.length).toBeGreaterThanOrEqual(1);
    expect(result.steps[0]?.type).toBe("thought");
  });

  it("returns tokens and cost from LLM usage", async () => {
    const layer = TestLLMServiceLayer([
      { match: "Task:", text: "FINAL ANSWER: Result." },
    ]);
    const result = await Effect.runPromise(
      executeReActKernel({ task: "Simple task" }).pipe(Effect.provide(layer)),
    );
    expect(result.totalTokens).toBeGreaterThan(0);
  });

  it("handles early end_turn termination on substantive response (no tools)", async () => {
    // end_turn with ≥50 chars and no tool call should terminate as "end_turn"
    const longResponse = "A".repeat(60);
    const layer = TestLLMServiceLayer([
      { match: "Task:", text: longResponse },
    ]);
    const result = await Effect.runPromise(
      executeReActKernel({ task: "Simple task", maxIterations: 3 }).pipe(
        Effect.provide(layer),
      ),
    );
    // Either end_turn or final_answer depending on mock behavior — just verify it terminates
    expect(["end_turn", "final_answer", "max_iterations"]).toContain(result.terminatedBy);
  });

  it("blocks execution of tools listed in blockedTools", async () => {
    // The model tries to call signal/send_message_to_user, which is blocked.
    // The kernel should return a synthetic BLOCKED observation instead of executing.
    const layer = TestLLMServiceLayer([
      { match: "Task:", toolCall: { name: "signal/send_message_to_user", args: { recipient: "+123", message: "hi" } } },
    ]);

    const result = await Effect.runPromise(
      executeReActKernel({
        task: "Send a message",
        maxIterations: 2,
        blockedTools: ["signal/send_message_to_user"],
      }).pipe(Effect.provide(layer)),
    );

    // Should have an observation with BLOCKED text
    const observations = result.steps.filter((s) => s.type === "observation");
    expect(observations.length).toBeGreaterThan(0);
    expect(observations[0]!.content).toContain("BLOCKED");
    expect(observations[0]!.content).toContain("signal/send_message_to_user");
  });

});

// ── reactKernel ThoughtKernel direct tests ────────────────────────────────────

describe("reactKernel (ThoughtKernel direct)", () => {
  /** Helper to build a minimal KernelContext for testing */
  function makeContext(overrides?: Partial<KernelContext>): KernelContext {
    const profile = CONTEXT_PROFILES["mid"];
    return {
      input: {
        task: "Test task",
      },
      profile,
      compression: {
        budget: profile.toolResultMaxChars ?? 800,
        previewItems: 3,
        autoStore: true,
        codeTransform: true,
      },
      toolService: { _tag: "None" },
      hooks: noopHooks,
      toolCallingDriver: new TextParseDriver(),
      ...overrides,
    };
  }

  it("thinking + FINAL ANSWER transitions to done", async () => {
    const layer = TestLLMServiceLayer([
      { match: "Task:", text: "FINAL ANSWER: The answer is 42." },
    ]);

    const state = initialKernelState({
      maxIterations: 3,
      strategy: "react-kernel",
      kernelType: "react",
    });

    const context = makeContext();

    const nextState = await Effect.runPromise(
      reactKernel(state, context).pipe(Effect.provide(layer)),
    );

    expect(nextState.status).toBe("done");
    expect(nextState.output).toBe("The answer is 42.");
    expect(nextState.steps.length).toBe(1);
    expect(nextState.steps[0]!.type).toBe("thought");
    expect(nextState.iteration).toBe(1);
  });

  it("thinking + tool call transitions to acting with pendingNativeToolCalls", async () => {
    const layer = TestLLMServiceLayer([
      { match: "Task:", toolCall: { name: "web-search", args: { query: "hello world" } } },
    ]);

    const state = initialKernelState({
      maxIterations: 3,
      strategy: "react-kernel",
      kernelType: "react",
    });

    // Inject a toolCallResolver so the native FC branch is activated
    const resolver = createToolCallResolver({ supportsToolCalling: true, supportsStreaming: true, supportsStructuredOutput: false, supportsLogprobs: false });
    const context = makeContext({
      input: { task: "Test task", toolCallResolver: resolver } as any,
    });

    const nextState = await Effect.runPromise(
      reactKernel(state, context).pipe(Effect.provide(layer)),
    );

    expect(nextState.status).toBe("acting");
    expect(nextState.steps.length).toBe(1);
    expect(nextState.steps[0]!.type).toBe("thought");
    // native FC: pendingNativeToolCalls stored in meta
    const pending = nextState.meta.pendingNativeToolCalls as Array<{ name: string; arguments: unknown }>;
    expect(pending[0]!.name).toBe("web-search");
  });

  it("fails fast on quota-vs-budget conflict before acting", async () => {
    const layer = TestLLMServiceLayer([
      { match: "Task:", toolCall: { name: "web-search", args: { query: "btc price" } } },
    ]);

    const state = initialKernelState({
      maxIterations: 3,
      strategy: "react-kernel",
      kernelType: "react",
    });

    const resolver = createToolCallResolver({
      supportsToolCalling: true,
      supportsStreaming: true,
      supportsStructuredOutput: false,
      supportsLogprobs: false,
    });
    const context = makeContext({
      input: {
        task: "Fetch 4 prices",
        toolCallResolver: resolver,
        requiredTools: ["web-search"],
        requiredToolQuantities: { "web-search": 4 },
        maxCallsPerTool: { "web-search": 2 },
        availableToolSchemas: [{ name: "web-search", description: "search", parameters: [] }],
      } as any,
    });

    const nextState = await Effect.runPromise(
      reactKernel(state, context).pipe(Effect.provide(layer)),
    );

    expect(nextState.status).toBe("failed");
    expect(nextState.error).toContain("Configuration conflict");
    expect(nextState.error).toContain("web-search");
    expect(nextState.error).toContain("required minCalls=4");
    expect(nextState.error).toContain("maxCallsPerTool=2");
  });

  it("acting transitions back to thinking after tool execution (native FC path)", async () => {
    const layer = TestLLMServiceLayer();

    // Start in acting state with pending native tool calls (FC path)
    const state: KernelState = {
      ...initialKernelState({
        maxIterations: 3,
        strategy: "react-kernel",
        kernelType: "react",
      }),
      status: "acting",
      steps: [
        {
          id: "test-step" as any,
          type: "thought",
          content: "I need to search for something.",
          timestamp: new Date(),
        },
      ],
      meta: {
        pendingNativeToolCalls: [{ id: "tc-1", name: "web-search", arguments: { query: "hello" } }],
        lastThought: "I need to search for something.",
        lastThinking: null,
      },
    };

    const context = makeContext();

    const nextState = await Effect.runPromise(
      reactKernel(state, context).pipe(Effect.provide(layer)),
    );

    // After native FC acting, agent returns to thinking
    expect(nextState.status).toBe("thinking");
    // Should have action + observation steps added
    const actionSteps = nextState.steps.filter((s) => s.type === "action");
    const obsSteps = nextState.steps.filter((s) => s.type === "observation");
    expect(actionSteps.length).toBe(1);
    expect(obsSteps.length).toBe(1);
    // Observation should report unavailable tool guard feedback
    expect(obsSteps[0]!.content).toContain("is not available in this run");
    // Iteration should have been incremented
    expect(nextState.iteration).toBe(1);
    // Guard-blocked tools should not be marked as used
    expect(nextState.toolsUsed.has("web-search")).toBe(false);
  });
});

describe("KernelState.controllerDecisionLog", () => {
  it("controllerDecisionLog starts empty and is a KernelState field", () => {
    const state = initialKernelState({ taskId: "t1", strategy: "reactive", kernelType: "react", maxIterations: 5 });
    expect(state.controllerDecisionLog).toEqual([]);
  });

  it("controllerDecisionLog accumulates across transitionState calls", () => {
    let state = initialKernelState({ taskId: "t1", strategy: "reactive", kernelType: "react", maxIterations: 5 });
    state = transitionState(state, {
      controllerDecisionLog: [...state.controllerDecisionLog, "early-stop: entropy converged"],
    });
    state = transitionState(state, {
      controllerDecisionLog: [...state.controllerDecisionLog, "compress: context at 0.92"],
    });
    expect(state.controllerDecisionLog).toHaveLength(2);
    expect(state.controllerDecisionLog[0]).toBe("early-stop: entropy converged");
    expect(state.controllerDecisionLog[1]).toBe("compress: context at 0.92");
  });

  it("formats decisions without reason field gracefully (no 'undefined' in output)", () => {
    // Simulate a compress decision which has no 'reason' field
    const compressDecision = { decision: "compress", sections: ["history", "memory"], estimatedSavings: 500 };
    const formatted = [compressDecision].map((d: Record<string, unknown>) => {
      const decision = String(d.decision ?? "");
      const context =
        typeof d.reason === "string" ? d.reason
        : "sections" in d && Array.isArray(d.sections) ? `sections=[${(d.sections as string[]).join(",")}]`
        : typeof d.skillName === "string" ? d.skillName
        : "";
      return context ? `${decision}: ${context}` : decision;
    });
    expect(formatted[0]).not.toContain("undefined");
    expect(formatted[0]).toContain("compress");
  });
});

describe("detectCompletionGaps", () => {
  it("does not require web-search when task says 'rag search'", () => {
    const toolsUsed = new Set(["rag-search"]);
    const gaps = detectCompletionGaps("try searching with rag search for memory", toolsUsed, []);
    expect(gaps.filter((g) => g.includes("web-search"))).toHaveLength(0);
  });

  it("does not require web-search when task says 'search' without an online/web qualifier", () => {
    const toolsUsed = new Set<string>();
    const gaps = detectCompletionGaps("search the agent memory", toolsUsed, []);
    expect(gaps.filter((g) => g.includes("web-search"))).toHaveLength(0);
  });

  it("requires web-search when task says 'search online'", () => {
    const toolsUsed = new Set<string>();
    const gaps = detectCompletionGaps("search online for the latest news", toolsUsed, []);
    expect(gaps.filter((g) => g.includes("web-search"))).toHaveLength(1);
  });

  it("requires web-search when task says 'look up'", () => {
    const toolsUsed = new Set<string>();
    const gaps = detectCompletionGaps("look up the weather today", toolsUsed, []);
    expect(gaps.filter((g) => g.includes("web-search"))).toHaveLength(1);
  });

  it("gap message includes the matched keyword, not an empty string", () => {
    const toolsUsed = new Set<string>();
    const gaps = detectCompletionGaps("look up the weather today", toolsUsed, []);
    const webSearchGap = gaps.find((g) => g.includes("web-search"))!;
    expect(webSearchGap).toBeDefined();
    // The gap message must NOT have an empty quoted string like `""`
    expect(webSearchGap).not.toMatch(/asks to ""\s/);
    expect(webSearchGap).toMatch(/look up/i);
  });
});
