/**
 * L12 — skills feature + agent assignment, wired to the stateful
 * `MockSkillGateway` (sharing a `MockAgentGateway`) via the real `DIProvider`.
 *
 * Covers:
 * - createSkill (project + global) → skill appears under the right scope
 * - updateSkill → content persisted in the gateway
 * - deleteSkill → skill removed from the list
 * - assignSkill / unassignSkill in AgentsPanel → reflected on the agent record
 * - guardrail "no direct invoke": SkillsPanel never offers a run/launch action
 *
 * Also includes MockSkillGateway unit tests.
 */

import { describe, it, expect } from "vitest";
import { render, screen, waitFor, fireEvent } from "@testing-library/react";

import {
  MockAgentGateway,
  MockProfileGateway,
  MockSkillGateway,
} from "@/adapters/mock";
import type { Gateways } from "@/ports";
import { DIProvider } from "@/app/di";
import { SkillsPanel } from "./SkillsPanel";
import { AgentsPanel } from "@/features/agents/AgentsPanel";

const PROJECT_ID = "proj-skill-test";

function renderSkillsPanel(agent?: MockAgentGateway, skill?: MockSkillGateway) {
  const a = agent ?? new MockAgentGateway();
  const sk = skill ?? new MockSkillGateway(a);
  const gateways = { agent: a, skill: sk } as unknown as Gateways;
  return {
    agent: a,
    skill: sk,
    ...render(
      <DIProvider gateways={gateways}>
        <SkillsPanel projectId={PROJECT_ID} />
      </DIProvider>,
    ),
  };
}

function renderAgentsPanel(agent: MockAgentGateway, skill: MockSkillGateway) {
  const profile = new MockProfileGateway();
  const gateways = { agent, profile, skill } as unknown as Gateways;
  return render(
    <DIProvider gateways={gateways}>
      <AgentsPanel projectId={PROJECT_ID} projectRoot="/tmp/proj" />
    </DIProvider>,
  );
}

async function waitForSkillsIdle() {
  await waitFor(() => {
    expect(screen.getByRole("button", { name: "create skill" })).toBeTruthy();
  });
}

/** Opens the SkillEditor, fills it, and saves. */
async function createSkill(
  name: string,
  content = "# Workflow",
  scope: "project" | "global" = "project",
) {
  await waitForSkillsIdle();
  fireEvent.click(screen.getByRole("button", { name: "create skill" }));
  await waitFor(() => expect(screen.getByLabelText("skill name")).toBeTruthy());
  fireEvent.change(screen.getByLabelText("skill name"), {
    target: { value: name },
  });
  fireEvent.change(screen.getByLabelText("skill content"), {
    target: { value: content },
  });
  fireEvent.change(screen.getByLabelText("skill scope"), {
    target: { value: scope },
  });
  fireEvent.click(screen.getByRole("button", { name: "Save skill" }));
}

// ---------------------------------------------------------------------------
// SkillsPanel feature tests
// ---------------------------------------------------------------------------

describe("SkillsPanel (with MockSkillGateway)", () => {
  it("shows empty state for both scopes initially", async () => {
    renderSkillsPanel();
    await waitForSkillsIdle();
    expect(screen.getByText("No project skills yet.")).toBeTruthy();
    expect(screen.getByText("No global skills yet.")).toBeTruthy();
  });

  it("creating a project skill adds it to the list", async () => {
    const { skill } = renderSkillsPanel();
    await createSkill("Refactor", "## steps", "project");
    await screen.findByText("Refactor");
    const projects = await skill.listSkills(PROJECT_ID, "project");
    expect(projects).toHaveLength(1);
    expect(projects[0].name).toBe("Refactor");
  });

  it("creating a global skill stores it under the global scope", async () => {
    const { skill } = renderSkillsPanel();
    await createSkill("Review", "## global", "global");
    await screen.findByText("Review");
    expect(await skill.listSkills(PROJECT_ID, "global")).toHaveLength(1);
    expect(await skill.listSkills(PROJECT_ID, "project")).toHaveLength(0);
  });

  it("Save is disabled when name or content is empty", async () => {
    renderSkillsPanel();
    await waitForSkillsIdle();
    fireEvent.click(screen.getByRole("button", { name: "create skill" }));
    await waitFor(() =>
      expect(screen.getByRole("button", { name: "Save skill" })).toBeTruthy(),
    );
    const save = screen.getByRole("button", {
      name: "Save skill",
    }) as HTMLButtonElement;
    expect(save.disabled).toBe(true);
  });

  it("editing a skill persists the new content", async () => {
    const { skill } = renderSkillsPanel();
    await createSkill("Editable", "old", "project");
    await screen.findByText("Editable");

    fireEvent.click(screen.getByRole("button", { name: "edit skill Editable" }));
    await waitFor(() =>
      expect(screen.getByLabelText("skill content")).toBeTruthy(),
    );
    fireEvent.change(screen.getByLabelText("skill content"), {
      target: { value: "new content" },
    });
    fireEvent.click(screen.getByRole("button", { name: "Save skill" }));

    await waitFor(async () => {
      const list = await skill.listSkills(PROJECT_ID, "project");
      expect(list[0].contentMd).toBe("new content");
    });
  });

  it("deleting a skill removes it from the list", async () => {
    renderSkillsPanel();
    await createSkill("ToDelete", "x", "project");
    await screen.findByText("ToDelete");
    fireEvent.click(
      screen.getByRole("button", { name: "delete skill ToDelete" }),
    );
    await waitFor(() => expect(screen.queryByText("ToDelete")).toBeNull());
  });

  it("guardrail: never offers a run/launch action for a skill (no direct invoke)", async () => {
    renderSkillsPanel();
    await createSkill("Plain", "x", "project");
    await screen.findByText("Plain");
    // Only Edit/Delete are exposed; nothing that would "run" the skill.
    expect(screen.queryByRole("button", { name: /^run/i })).toBeNull();
    expect(screen.queryByRole("button", { name: /^launch/i })).toBeNull();
    expect(screen.queryByRole("button", { name: /invoke/i })).toBeNull();
  });
});

// ---------------------------------------------------------------------------
// Assignment integration (AgentsPanel)
// ---------------------------------------------------------------------------

describe("Skill assignment (AgentsPanel + MockSkillGateway)", () => {
  it("assigns a skill to the selected agent and shows it as a chip", async () => {
    const agent = new MockAgentGateway();
    const skill = new MockSkillGateway(agent);
    const created = await agent.createAgent(PROJECT_ID, {
      name: "Worker",
      profileId: "p1",
    });
    await skill.createSkill({
      projectId: PROJECT_ID,
      name: "Deploy",
      content: "## deploy",
      scope: "project",
    });

    renderAgentsPanel(agent, skill);
    await waitFor(() => expect(screen.getByLabelText("agent name")).toBeTruthy());

    // Select the agent
    fireEvent.click(screen.getByText("Worker"));
    await waitFor(() =>
      expect(screen.getByLabelText("skill to assign")).toBeTruthy(),
    );

    // Choose the skill and assign
    const sk = (await skill.listSkills(PROJECT_ID, "project"))[0];
    fireEvent.change(screen.getByLabelText("skill to assign"), {
      target: { value: sk.id },
    });
    fireEvent.click(screen.getByRole("button", { name: "assign skill" }));

    // The agent record now carries the skill ref
    await waitFor(async () => {
      const agents = await agent.listAgents(PROJECT_ID);
      const a = agents.find((x) => x.id === created.id)!;
      expect(a.skills.map((s) => s.skillId)).toContain(sk.id);
    });
    // Chip with an unassign control appears
    expect(screen.getByRole("button", { name: "unassign Deploy" })).toBeTruthy();
  });

  it("unassigns a skill from the agent", async () => {
    const agent = new MockAgentGateway();
    const skill = new MockSkillGateway(agent);
    const a = await agent.createAgent(PROJECT_ID, {
      name: "Worker2",
      profileId: "p1",
    });
    const sk = await skill.createSkill({
      projectId: PROJECT_ID,
      name: "Cleanup",
      content: "## cleanup",
      scope: "global",
    });
    await skill.assignSkill(PROJECT_ID, a.id, sk.id, sk.scope);

    renderAgentsPanel(agent, skill);
    await waitFor(() => expect(screen.getByLabelText("agent name")).toBeTruthy());
    fireEvent.click(screen.getByText("Worker2"));

    await waitFor(() =>
      expect(
        screen.getByRole("button", { name: "unassign Cleanup" }),
      ).toBeTruthy(),
    );
    fireEvent.click(screen.getByRole("button", { name: "unassign Cleanup" }));

    await waitFor(async () => {
      const agents = await agent.listAgents(PROJECT_ID);
      expect(agents.find((x) => x.id === a.id)!.skills).toHaveLength(0);
    });
  });
});

// ---------------------------------------------------------------------------
// MockSkillGateway unit tests
// ---------------------------------------------------------------------------

describe("MockSkillGateway (unit)", () => {
  it("scopes are isolated: a project skill never appears in global and vice-versa", async () => {
    const gw = new MockSkillGateway(new MockAgentGateway());
    await gw.createSkill({
      projectId: "p",
      name: "P",
      content: "x",
      scope: "project",
    });
    await gw.createSkill({
      projectId: "p",
      name: "G",
      content: "y",
      scope: "global",
    });
    expect(await gw.listSkills("p", "project")).toHaveLength(1);
    expect(await gw.listSkills("p", "global")).toHaveLength(1);
    expect((await gw.listSkills("p", "project"))[0].name).toBe("P");
    expect((await gw.listSkills("p", "global"))[0].name).toBe("G");
  });

  it("updateSkill throws NOT_FOUND for unknown skill", async () => {
    const gw = new MockSkillGateway(new MockAgentGateway());
    await expect(
      gw.updateSkill("p", "project", "ghost", "x"),
    ).rejects.toMatchObject({ code: "NOT_FOUND" });
  });

  it("assignSkill is idempotent (no duplicate refs)", async () => {
    const agentGw = new MockAgentGateway();
    const gw = new MockSkillGateway(agentGw);
    const a = await agentGw.createAgent("p", { name: "A", profileId: "p1" });
    const s = await gw.createSkill({
      projectId: "p",
      name: "S",
      content: "x",
      scope: "project",
    });
    await gw.assignSkill("p", a.id, s.id, s.scope);
    await gw.assignSkill("p", a.id, s.id, s.scope);
    const agents = await agentGw.listAgents("p");
    expect(agents[0].skills).toHaveLength(1);
  });

  it("assignSkill throws NOT_FOUND for unknown agent", async () => {
    const gw = new MockSkillGateway(new MockAgentGateway());
    await expect(
      gw.assignSkill("p", "ghost", "s", "project"),
    ).rejects.toMatchObject({ code: "NOT_FOUND" });
  });
});
