import type { SessionState } from "@coder-studio/core";
import { describe, expect, it, vi } from "vitest";
import type { SessionManager } from "../session/manager.js";
import type { TerminalManager } from "../terminal/manager.js";
import { describeNonInjectableState, SupervisorInjector } from "./injector.js";

function makeInjector(sendInputSpy = vi.fn(), state: SessionState = "running") {
  const sessionMgr = {
    get: vi.fn(() => ({
      id: "sess-1",
      terminalId: "term-1",
      state,
      workspaceId: "ws-1",
      providerId: "claude",
      capability: "full",
      startedAt: 1,
      lastActiveAt: 1,
    })),
    sendInput: sendInputSpy,
  } as unknown as SessionManager;

  return new SupervisorInjector({
    sessionMgr,
    terminalMgr: {} as unknown as TerminalManager,
  });
}

const supervisor = {
  id: "sup-1",
  sessionId: "sess-1",
  workspaceId: "ws-1",
  targetId: "tgt-1",
  state: "idle" as const,
  objective: "Finish the repo migration",
  evaluatorProviderId: "claude",
  maxSupervisionCount: 0,
  completedSupervisionCount: 0,
  recentTargetCycles: [],
  createdAt: 1,
  updatedAt: 1,
};

describe("SupervisorInjector", () => {
  it("writes message through sessionMgr.sendInput using internal_submit activity", async () => {
    const sendInputSpy = vi.fn();
    const injector = makeInjector(sendInputSpy);

    await injector.inject(
      supervisor,
      {
        message: "Wire the repos into SupervisorManager next.",
      },
      []
    );

    expect(sendInputSpy).toHaveBeenCalledWith("sess-1", expect.any(Buffer), "internal_submit");
    const buffer = sendInputSpy.mock.calls[0]![1] as Buffer;
    const payload = buffer.toString("utf8");
    expect(payload).toContain("[Supervisor] Wire the repos into SupervisorManager next.");
    expect(payload).not.toContain("Objective:");
    expect(payload).not.toContain("Assessment:");
  });

  it("passes multi-line messages with bracketed paste + CR", async () => {
    const sendInputSpy = vi.fn();
    const injector = makeInjector(sendInputSpy);

    await injector.inject(
      supervisor,
      {
        message: "Do:\n  1. step one\n  2. step two",
      },
      []
    );

    const buffer = sendInputSpy.mock.calls[0]![1] as Buffer;
    const payload = buffer.toString("utf8");
    expect(payload.startsWith("\x1b[200~")).toBe(true);
    expect(payload.endsWith("\x1b[201~\r")).toBe(true);
  });

  it("still injects when the same guidance was recently injected", async () => {
    const sendInputSpy = vi.fn();
    const injector = makeInjector(sendInputSpy);

    const result = await injector.inject(
      supervisor,
      {
        message: "Run the focused parser test.",
      },
      [
        {
          id: "cycle-0",
          supervisorId: "sup-1",
          sessionId: "sess-1",
          status: "injected",
          trigger: "manual",
          evidenceSource: "headless_snapshot",
          objective: "Finish the repo migration",
          evaluatorProviderId: "claude",
          injectedGuidance: "[Supervisor] Run the focused parser test.",
          createdAt: 0,
          completedAt: 0,
        },
      ]
    );

    expect(result.injected).toBe(true);
    expect(sendInputSpy).toHaveBeenCalledTimes(1);
  });

  it("refuses to write into a session that has not finished handshake (state=starting)", async () => {
    const sendInputSpy = vi.fn();
    const injector = makeInjector(sendInputSpy, "starting");

    await expect(injector.inject(supervisor, { message: "go" }, [])).rejects.toMatchObject({
      code: "inject_target_unavailable",
      message: expect.stringContaining("still starting up"),
    });

    expect(sendInputSpy).not.toHaveBeenCalled();
  });

  it("does not send input when the abort signal is already cancelled", async () => {
    const sendInputSpy = vi.fn();
    const injector = makeInjector(sendInputSpy);
    const controller = new AbortController();
    controller.abort();

    await expect(
      injector.inject(supervisor, { message: "go" }, [], { signal: controller.signal })
    ).rejects.toMatchObject({
      code: "supervisor_eval_aborted",
    });

    expect(sendInputSpy).not.toHaveBeenCalled();
  });
});

describe("describeNonInjectableState", () => {
  it("returns actionable copy for every non-injectable session state", () => {
    expect(describeNonInjectableState("starting")).toMatch(/starting up/);
    expect(describeNonInjectableState("ended")).toMatch(/ended/);
    expect(describeNonInjectableState("draft")).toMatch(/draft/);
  });
});
