/**
 * Tests for skills v2.1 plus Session comms convenience methods.
 */

import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { DeferredSession, Session } from "../dist/session.js";
import { MeerkatClient } from "../dist/client.js";

describe("Skills v2.1", () => {
  it("Session.invokeSkill sends structured skillRefs to _startTurn", async () => {
    const calls = [];
    const mockClient = {
      hasCapability(name) {
        return name === "skills";
      },
      requireCapability(name) {
        if (!this.hasCapability(name)) {
          throw new Error(`Missing capability: ${name}`);
        }
      },
      async _startTurn(sessionId, prompt, options) {
        calls.push({ sessionId, prompt, options });
        return {
          sessionId: "s-1",
          text: "ok",
          turns: 1,
          toolCalls: 0,
          usage: { inputTokens: 10, outputTokens: 5 },
        };
      },
    };

    const initialResult = {
      sessionId: "s-1",
      text: "init",
      turns: 0,
      toolCalls: 0,
      usage: { inputTokens: 0, outputTokens: 0 },
    };
    const session = new Session(mockClient, initialResult);

    const result = await session.invokeSkill(
      { sourceUuid: "abc-123", skillName: "email-extractor" },
      "run the extractor",
    );

    assert.equal(result.text, "ok");
    assert.equal(calls.length, 1);
    assert.equal(calls[0].prompt, "run the extractor");
    assert.deepEqual(calls[0].options.skillRefs, [
      { sourceUuid: "abc-123", skillName: "email-extractor" },
    ]);
  });

  it("Session.invokeSkill rejects legacy string refs", async () => {
    const mockClient = {
      hasCapability() { return true; },
      requireCapability() {},
      async _startTurn() {
        throw new Error("_startTurn should not be called");
      },
    };

    const session = new Session(mockClient, {
      sessionId: "s-1", text: "init", turns: 0, toolCalls: 0,
      usage: { inputTokens: 0, outputTokens: 0 },
    });

    await assert.rejects(
      () => session.invokeSkill(
        "dc256086-0d2f-4f61-a307-320d4148107f/email-extractor",
        "run",
      ),
      /Skill references must be SkillKey objects/,
    );
  });

  it("parseRunResult includes skillDiagnostics", () => {
    const raw = {
      session_id: "s1",
      text: "Hello!",
      turns: 1,
      tool_calls: 0,
      usage: { input_tokens: 100, output_tokens: 50 },
      skill_diagnostics: {
        source_health: {
          state: "healthy",
          invalid_ratio: 0.0,
          invalid_count: 0,
          total_count: 10,
          failure_streak: 0,
          handshake_failed: false,
        },
        quarantined: [],
      },
    };
    const result = MeerkatClient.parseRunResult(raw);
    assert.deepEqual(result.skillDiagnostics, {
      sourceHealth: {
        state: "healthy",
        invalidRatio: 0,
        invalidCount: 0,
        totalCount: 10,
        failureStreak: 0,
        handshakeFailed: false,
      },
      quarantined: [],
    });
  });

  it("parseRunResult handles missing skillDiagnostics", () => {
    const raw = {
      session_id: "s1",
      text: "Hello!",
      turns: 1,
      tool_calls: 0,
      usage: { input_tokens: 100, output_tokens: 50 },
    };
    const result = MeerkatClient.parseRunResult(raw);
    assert.equal(result.skillDiagnostics, undefined);
  });

  it("Session.send forwards command through _send", async () => {
    const calls = [];
    const mockClient = {
      async _send(sessionId, command) {
        calls.push({ sessionId, command });
        return { queued: true };
      },
    };

    const session = new Session(mockClient, {
      sessionId: "s-1", text: "init", turns: 0, toolCalls: 0,
      usage: { inputTokens: 0, outputTokens: 0 },
    });

    const result = await session.send({
      kind: "peer_message",
      to: "agent-b",
      body: "hello",
    });

    assert.deepEqual(result, { queued: true });
    assert.equal(calls.length, 1);
    assert.equal(calls[0].sessionId, "s-1");
    assert.deepEqual(calls[0].command, {
      kind: "peer_message",
      to: "agent-b",
      body: "hello",
    });
  });

  it("Session.send forwards peer_lifecycle commands", async () => {
    const calls = [];
    const mockClient = {
      async _send(sessionId, command) {
        calls.push({ sessionId, command });
        return { queued: true };
      },
    };

    const session = new Session(mockClient, {
      sessionId: "s-1", text: "init", turns: 0, toolCalls: 0,
      usage: { inputTokens: 0, outputTokens: 0 },
    });

    const result = await session.send({
      kind: "peer_lifecycle",
      to: "agent-b",
      lifecycle_kind: "mob.peer_retired",
      params: { reason: "done" },
    });

    assert.deepEqual(result, { queued: true });
    assert.deepEqual(calls, [{
      sessionId: "s-1",
      command: {
        kind: "peer_lifecycle",
        to: "agent-b",
        lifecycle_kind: "mob.peer_retired",
        params: { reason: "done" },
      },
    }]);
  });

  it("Session.peers returns peers list from _peers", async () => {
    const calls = [];
    const mockClient = {
      async _peers(sessionId) {
        calls.push(sessionId);
        return { peers: [{ id: "peer-a" }, { id: "peer-b" }] };
      },
    };

    const session = new Session(mockClient, {
      sessionId: "s-1", text: "init", turns: 0, toolCalls: 0,
      usage: { inputTokens: 0, outputTokens: 0 },
    });

    const peers = await session.peers();
    assert.deepEqual(peers, [{ id: "peer-a" }, { id: "peer-b" }]);
    assert.deepEqual(calls, ["s-1"]);
  });

  it("Session.history forwards to readSessionHistory", async () => {
    const calls = [];
    const mockClient = {
      async readSessionHistory(sessionId, options) {
        calls.push({ sessionId, options });
        return {
          sessionId,
          sessionRef: "ref-1",
          messageCount: 2,
          offset: options?.offset ?? 0,
          limit: options?.limit,
          hasMore: false,
          messages: [
            { role: "user", content: "hello", toolCalls: [], blocks: [], results: [] },
            { role: "assistant", content: "ok", stopReason: "end_turn", toolCalls: [], blocks: [], results: [] },
          ],
        };
      },
    };

    const session = new Session(mockClient, {
      sessionId: "s-1", text: "init", turns: 0, toolCalls: 0,
      usage: { inputTokens: 0, outputTokens: 0 },
    });

    const history = await session.history({ offset: 1, limit: 5 });

    assert.equal(history.sessionId, "s-1");
    assert.equal(history.messages[1].role, "assistant");
    assert.deepEqual(calls, [{ sessionId: "s-1", options: { offset: 1, limit: 5 } }]);
  });

  it("DeferredSession.history forwards to readSessionHistory", async () => {
    const calls = [];
    const mockClient = {
      async readSessionHistory(sessionId, options) {
        calls.push({ sessionId, options });
        return {
          sessionId,
          sessionRef: "ref-2",
          messageCount: 1,
          offset: options?.offset ?? 0,
          limit: options?.limit,
          hasMore: false,
          messages: [
            { role: "assistant", content: "pending", stopReason: "end_turn", toolCalls: [], blocks: [], results: [] },
          ],
        };
      },
    };

    const session = new DeferredSession(mockClient, "s-2", "ref-2");
    const history = await session.history({ offset: 2, limit: 3 });

    assert.equal(history.sessionId, "s-2");
    assert.equal(history.sessionRef, "ref-2");
    assert.deepEqual(calls, [{ sessionId: "s-2", options: { offset: 2, limit: 3 } }]);
  });

  it("createSessionStreaming buffers early events before session id is bound", async () => {
    const client = new MeerkatClient();
    client.process = { stdin: { write() {} } };

    let resolveResponse;
    client.registerRequest = () =>
      new Promise((resolve) => {
        resolveResponse = resolve;
      });

    const stream = client.createSessionStreaming("hello");
    client.handleLine(
      JSON.stringify({
        method: "session/event",
        params: {
          session_id: "s-early",
          event: { type: "text_delta", delta: "early" },
        },
      }),
    );
    resolveResponse({
      session_id: "s-early",
      text: "done",
      turns: 1,
      tool_calls: 0,
      usage: { input_tokens: 1, output_tokens: 1 },
    });

    const [text, result] = await stream.collectText();
    assert.equal(text, "early");
    assert.equal(result.sessionId, "s-early");
  });

  it("createSessionStreaming does not clear buffered standalone stream events", async () => {
    const client = new MeerkatClient();
    client.process = { stdin: { write() {} } };
    client.unmatchedStandaloneStreamBuffer.set("stream-1", [{ event_id: "e1" }]);
    client.registerRequest = () => Promise.resolve({ session_id: "s-created" });

    client.createSessionStreaming("hello");
    await new Promise((resolve) => setImmediate(resolve));

    assert.deepEqual(client.unmatchedStandaloneStreamBuffer.get("stream-1"), [{ event_id: "e1" }]);
  });

  it("config wrappers preserve config envelope metadata fields", async () => {
    const client = new MeerkatClient();
    const calls = [];
    client.request = async (method, params) => {
      calls.push({ method, params });
      return {
        config: { agent: { model: "x" } },
        generation: 3,
        realm_id: "realm-alpha",
        instance_id: "instance-42",
        backend: "sqlite",
        resolved_paths: {
          root: "/tmp/realm-alpha",
          config: "/tmp/realm-alpha/config.toml",
        },
      };
    };

    const getResponse = await client.getConfig();
    const setResponse = await client.setConfig(
      { agent: { model: "x" } },
      { expectedGeneration: 2 },
    );
    const patchResponse = await client.patchConfig(
      { agent: { max_tokens_per_turn: 4096 } },
      { expectedGeneration: 3 },
    );

    assert.equal(calls.length, 3);
    assert.equal(calls[0].method, "config/get");
    assert.deepEqual(calls[0].params, {});
    assert.equal(calls[1].method, "config/set");
    assert.equal(calls[1].params.expected_generation, 2);
    assert.equal(calls[2].method, "config/patch");
    assert.equal(calls[2].params.expected_generation, 3);

    for (const response of [getResponse, setResponse, patchResponse]) {
      assert.equal(response.generation, 3);
      assert.equal(response.realmId, "realm-alpha");
      assert.equal(response.instanceId, "instance-42");
      assert.equal(response.backend, "sqlite");
      assert.deepEqual(response.resolvedPaths, {
        root: "/tmp/realm-alpha",
        config: "/tmp/realm-alpha/config.toml",
      });
    }
  });


  it("buffers standalone stream termination until the subscription queue is registered", async () => {
    const client = new MeerkatClient();
    client.request = async (method, params) => {
      if (method === "session/stream_open") {
        client.handleLine(JSON.stringify({
          jsonrpc: "2.0",
          method: "session/stream_end",
          params: {
            stream_id: "stream-end-1",
            ended: true,
            outcome: "remote_end",
          },
        }));
        return { stream_id: "stream-end-1" };
      }
      if (method === "session/stream_close") {
        return { closed: true };
      }
      return {};
    };

    const sub = await client.subscribeSessionEvents("s1");
    const iterator = sub[Symbol.asyncIterator]();
    const first = await Promise.race([
      iterator.next(),
      new Promise((_, reject) => setTimeout(() => reject(new Error("stream did not terminate")), 200)),
    ]);
    assert.equal(first.done, true);
    assert.deepEqual(sub.terminalOutcome, {
      stream_id: "stream-end-1",
      ended: true,
      outcome: "remote_end",
    });
    await sub.close();
  });

  it("buffers standalone stream notifications until the subscription queue is registered", async () => {
    const client = new MeerkatClient();
    client.request = async (method, params) => {
      if (method === "session/stream_open") {
        client.handleLine(JSON.stringify({
          jsonrpc: "2.0",
          method: "session/stream_event",
          params: {
            stream_id: "stream-1",
            event: { event_id: "e1", payload: { type: "text_delta", delta: "hi" } },
          },
        }));
        return { stream_id: "stream-1" };
      }
      if (method === "session/stream_close") {
        return { closed: true };
      }
      return {};
    };

    const sub = await client.subscribeSessionEvents("s1");
    const iterator = sub[Symbol.asyncIterator]();
    const first = await iterator.next();
    assert.equal(first.done, false);
    assert.equal(first.value.payload.type, "text_delta");
    assert.equal(first.value.payload.delta, "hi");
    await sub.close();
  });

  it("preserves buffered standalone stream notification order", async () => {
    const client = new MeerkatClient();
    client.request = async (method, params) => {
      if (method === "session/stream_open") {
        client.handleLine(JSON.stringify({
          jsonrpc: "2.0",
          method: "session/stream_event",
          params: {
            stream_id: "stream-ordered",
            event: { event_id: "e1", payload: { type: "text_delta", delta: "hi" } },
          },
        }));
        client.handleLine(JSON.stringify({
          jsonrpc: "2.0",
          method: "session/stream_event",
          params: {
            stream_id: "stream-ordered",
            event: { event_id: "e2", payload: { type: "text_delta", delta: "there" } },
          },
        }));
        return { stream_id: "stream-ordered" };
      }
      if (method === "session/stream_close") {
        return { closed: true };
      }
      return {};
    };

    const sub = await client.subscribeSessionEvents("s1");
    const iterator = sub[Symbol.asyncIterator]();
    const first = await iterator.next();
    const second = await iterator.next();
    assert.equal(first.done, false);
    assert.equal(second.done, false);
    assert.equal(first.value.payload.type, "text_delta");
    assert.equal(first.value.payload.delta, "hi");
    assert.equal(second.value.payload.type, "text_delta");
    assert.equal(second.value.payload.delta, "there");
    await sub.close();
  });

  it("createMob routes through the typed mob/create RPC method", async () => {
    const client = new MeerkatClient();
    const calls = [];
    client.request = async (method, params) => {
      calls.push({ method, params });
      if (method === "mob/create") {
        return { ok: true, mob_id: "m1" };
      }
      return {};
    };
    client._methods = new Set(["mob/create"]);
    client._capabilities = [];

    const mob = await client.createMob({
      definition: { id: "mob-1", profiles: { worker: { model: "claude-sonnet-4-6" } } },
    });

    assert.equal(mob.mobId, "m1");
    assert.deepEqual(calls, [
      {
        method: "mob/create",
        params: { definition: { id: "mob-1", profiles: { worker: { model: "claude-sonnet-4-6" } } } },
      },
    ]);
  });
});
