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

import {
  MockAgentGateway,
  MockPermissionGateway,
  MockProfileGateway,
} from "@/adapters/mock";
import { DIProvider } from "@/app/di";
import type { Gateways } from "@/ports";
import { PermissionsPanel } from "./PermissionsPanel";

const PROJECT_ID = "proj-permissions-test";

async function renderPanel() {
  const agent = new MockAgentGateway();
  const permission = new MockPermissionGateway();
  await agent.createAgent(PROJECT_ID, { name: "Builder", profileId: "p1" });
  const gateways = {
    agent,
    permission,
    profile: new MockProfileGateway(),
  } as unknown as Gateways;

  render(
    <DIProvider gateways={gateways}>
      <PermissionsPanel projectId={PROJECT_ID} />
    </DIProvider>,
  );

  await waitFor(() => {
    expect(screen.getByText("Builder")).toBeTruthy();
  });

  return { agent, permission };
}

function clickChoice(groupName: string, choice: string) {
  const group = screen.getByRole("group", { name: groupName });
  fireEvent.click(within(group).getByRole("button", { name: choice }));
}

describe("PermissionsPanel", () => {
  it("saves project defaults from the compact policy editor", async () => {
    const { permission } = await renderPanel();

    fireEvent.change(screen.getByLabelText("Project defaults fallback"), {
      target: { value: "allow" },
    });
    clickChoice("Read permission", "Allow");
    clickChoice("Bash permission", "Deny");
    fireEvent.click(screen.getByRole("button", { name: "Save" }));

    await waitFor(async () => {
      const doc = await permission.getProjectPermissions(PROJECT_ID);
      expect(doc.projectDefaults?.fallback).toBe("allow");
      expect(doc.projectDefaults?.rules).toEqual([
        { capability: "read", effect: "allow", paths: ["**"] },
        { capability: "executeBash", effect: "deny", commands: [] },
      ]);
    });
  });

  it("saves and clears an agent override", async () => {
    const { permission } = await renderPanel();

    fireEvent.click(screen.getByText("Builder"));
    fireEvent.change(screen.getByLabelText("Override — Builder fallback"), {
      target: { value: "deny" },
    });
    clickChoice("Write permission", "Allow");
    fireEvent.click(screen.getByRole("button", { name: "Save" }));

    await waitFor(async () => {
      const doc = await permission.getProjectPermissions(PROJECT_ID);
      expect(doc.agents?.[0]?.permissions.fallback).toBe("deny");
      expect(doc.agents?.[0]?.permissions.rules).toEqual([
        { capability: "write", effect: "allow", paths: ["**"] },
      ]);
    });

    fireEvent.click(screen.getByRole("button", { name: "Clear" }));

    await waitFor(async () => {
      const doc = await permission.getProjectPermissions(PROJECT_ID);
      expect(doc.agents).toEqual([]);
    });
  });

  it("saves project network defaults in the separate system permissions document", async () => {
    const { permission } = await renderPanel();

    fireEvent.change(screen.getByLabelText("Réseau — defaults projet network"), {
      target: { value: "allow" },
    });
    fireEvent.click(screen.getByRole("button", { name: "Save network" }));

    await waitFor(async () => {
      const doc = await permission.getProjectSystemPermissions(PROJECT_ID);
      expect(doc.projectDefault?.network).toBe("allow");
    });
    expect(screen.getAllByText("Réseau autorisé").length).toBeGreaterThan(0);
  });

  it("renders the network control read-only when persisted control says so", async () => {
    const agent = new MockAgentGateway();
    const permission = new MockPermissionGateway();
    const created = await agent.createAgent(PROJECT_ID, {
      name: "Builder",
      profileId: "p1",
    });
    permission.setResolvedAgentSystemPermissions(created.id, {
      wanted: "allow",
      effective: "deny",
      runtimeLock: {
        state: "locked",
        source: "external-runtime",
        reason: "Runtime network is locked.",
      },
      control: { mode: "readOnly", reason: "Runtime network is locked." },
      runtimeControl: { mode: "readOnly", reason: "Runtime network is locked." },
    });
    const gateways = {
      agent,
      permission,
      profile: new MockProfileGateway(),
    } as unknown as Gateways;

    render(
      <DIProvider gateways={gateways}>
        <PermissionsPanel projectId={PROJECT_ID} />
      </DIProvider>,
    );

    await waitFor(() => {
      expect(screen.getByText("Runtime verrouillé")).toBeTruthy();
    });
    expect(screen.getByText("Runtime network is locked.")).toBeTruthy();
    expect(
      (screen.getByLabelText("Réseau — defaults projet network") as HTMLSelectElement)
        .disabled,
    ).toBe(true);
  });

  it("keeps wanted network editable when no runtime is active", async () => {
    const { permission } = await renderPanel();

    await waitFor(() => {
      expect(screen.getByText("Aucun runtime actif")).toBeTruthy();
    });
    expect(
      screen.getByText(
        "Aucun runtime actif. La configuration sera appliquée au prochain lancement.",
      ),
    ).toBeTruthy();
    expect(
      (screen.getByLabelText("Réseau — defaults projet network") as HTMLSelectElement)
        .disabled,
    ).toBe(false);

    fireEvent.change(screen.getByLabelText("Réseau — defaults projet network"), {
      target: { value: "deny" },
    });
    fireEvent.click(screen.getByRole("button", { name: "Save network" }));

    await waitFor(async () => {
      const doc = await permission.getProjectSystemPermissions(PROJECT_ID);
      expect(doc.projectDefault?.network).toBe("deny");
    });
  });

  it("keeps an idle agent editable by default and only enables Save after a change", async () => {
    await renderPanel();

    fireEvent.click(screen.getByText("Builder"));

    await waitFor(() => {
      expect(screen.getByText("Aucun runtime actif")).toBeTruthy();
    });
    expect(screen.queryByText("Lecture seule")).toBeNull();
    expect(screen.queryByText("Runtime verrouillé")).toBeNull();

    const select = screen.getByLabelText("Réseau — Builder network") as HTMLSelectElement;
    const save = screen.getByRole("button", { name: "Save network" });

    expect(select.disabled).toBe(false);
    expect(save).toHaveProperty("disabled", true);

    fireEvent.change(select, { target: { value: "allow" } });

    expect(save).toHaveProperty("disabled", false);
  });

  it("keeps wanted network editable when only runtime control is read-only", async () => {
    const agent = new MockAgentGateway();
    const permission = new MockPermissionGateway();
    const created = await agent.createAgent(PROJECT_ID, {
      name: "Builder",
      profileId: "p1",
    });
    permission.setResolvedAgentSystemPermissions(created.id, {
      wanted: "allow",
      effective: "deny",
      runtimeLock: {
        state: "locked",
        source: "external-runtime",
        reason: "IdeA cannot change this active runtime.",
      },
      control: { mode: "editable" },
      runtimeControl: {
        mode: "readOnly",
        reason: "IdeA cannot change this active runtime.",
      },
    });
    const gateways = {
      agent,
      permission,
      profile: new MockProfileGateway(),
    } as unknown as Gateways;

    render(
      <DIProvider gateways={gateways}>
        <PermissionsPanel projectId={PROJECT_ID} />
      </DIProvider>,
    );

    await waitFor(() => {
      expect(screen.getByText("Runtime verrouillé")).toBeTruthy();
    });
    expect(
      (screen.getByLabelText("Réseau — defaults projet network") as HTMLSelectElement)
        .disabled,
    ).toBe(false);
  });
});
