import { ToolGroup } from "../src/model/config/permissions.js";
import {
  buildConfig,
  getTestHarness,
  TestHarness,
  transportTypes,
} from "./utils.js";
import { Permissions } from "@mcpx/shared-model";

describe.each(transportTypes)("ACL over %s Router", (transportType) => {
  const toolGroups: ToolGroup[] = [
    { name: "admin", services: { "calculator-service": ["powerOfTwo"] } },
    { name: "read-only", services: { "echo-service": ["echo"] } },
  ];
  const permissions: Permissions = {
    default: { _type: "default-block", allow: [] },
    consumers: {
      "power-users": {
        _type: "default-allow",
        block: ["admin"],
      },
      guests: {
        _type: "default-block",
        allow: ["read-only"],
      },
    },
    clientNames: {},
  };
  // Define the cases for testing ACL behavior
  interface Invocation {
    _type: "success" | "failed";
    name: string;
    params: Record<string, unknown>;
    response: string;
  }

  function buildEchoInvocation(_type: "success" | "failed"): Invocation {
    return {
      _type,
      name: "echo-service__echo",
      params: { message: "The sound of silence?" },
      response: "Tool echo: The sound of silence?",
    };
  }
  function buildAddInvocation(_type: "success" | "failed"): Invocation {
    return {
      _type,
      name: "calculator-service__add",
      params: { a: 5, b: 2 },
      response: "Result: 7",
    };
  }

  function buildPowerOfTwoInvocation(_type: "success" | "failed"): Invocation {
    return {
      _type,
      name: "calculator-service__powerOfTwo",
      params: { base: 4 },
      response: "Result: 16",
    };
  }

  interface Case {
    headers: { "x-lunar-consumer-tag"?: string };
    visibleTools: string[];
    invocations: Invocation[];
  }
  const cases: Case[] = [
    {
      headers: {},
      visibleTools: [],
      invocations: [
        buildAddInvocation("failed"),
        buildPowerOfTwoInvocation("failed"),
        buildEchoInvocation("failed"),
      ],
    },
    {
      headers: { "x-lunar-consumer-tag": "unrelated" },
      visibleTools: [],
      invocations: [
        buildAddInvocation("failed"),
        buildPowerOfTwoInvocation("failed"),
        buildEchoInvocation("failed"),
      ],
    },
    {
      headers: { "x-lunar-consumer-tag": "power-users" },
      visibleTools: ["calculator-service__add", "echo-service__echo"], // Order is alphabetical so this should not be flaky
      invocations: [
        buildAddInvocation("success"),
        buildPowerOfTwoInvocation("failed"),
        buildEchoInvocation("success"),
      ],
    },
    {
      headers: { "x-lunar-consumer-tag": "guests" },
      visibleTools: ["echo-service__echo"],
      invocations: [
        buildAddInvocation("failed"),
        buildPowerOfTwoInvocation("failed"),
        buildEchoInvocation("success"),
      ],
    },
  ];
  cases.forEach(({ headers, visibleTools, invocations }) => {
    describe(`when consumer tag header is "${headers["x-lunar-consumer-tag"] || "not passed"}"`, () => {
      let testHarness: TestHarness;
      beforeAll(async () => {
        const config = buildConfig({ toolGroups, permissions });
        testHarness = getTestHarness({
          config,
          clientConnectExtraHeaders: headers,
        });
        await testHarness.initialize(transportType);
      });

      afterAll(async () => {
        await testHarness.shutdown();
      });

      it("lists only allowed tools", async () => {
        const tools = await testHarness.client.listTools();
        expect(tools.tools.map((t) => t.name)).toEqual(visibleTools);
      });

      it("allows calling only allowed tools", async () => {
        await Promise.all(
          invocations.map(async (invocation) => {
            const { name, params, response, _type } = invocation;
            if (_type === "failed") {
              return;
            }

            const res = await testHarness.client.callTool({
              name,
              arguments: params,
            });
            const result = (res.content as [{ text: string }])[0];
            expect(result.text).toEqual(response);
          }),
        );
      });

      it("rejects calling disallowed tools", async () => {
        await Promise.all(
          invocations.map(async (invocation) => {
            const { name, params, _type } = invocation;
            if (_type === "success") {
              return;
            }

            await expect(
              testHarness.client.callTool({
                name,
                arguments: params,
              }),
            ).rejects.toThrow();
            return;
          }),
        );
      });
    });
  });

  describe("clientName-level permission (no consumer tag header)", () => {
    // The test harness constructs its MCP client as
    //   new Client({ name: "end-client", version: "1.0.0" })
    // (see it/utils.ts). The MCP SDK sends that name as `clientInfo.name` in
    // the initialize RPC, which mcpx-server extracts into session.metadata
    // and mcp-gateway then passes into permissionManager.hasPermission.
    //
    // Config: default BLOCKS everything. Only the clientNames["end-client"]
    // entry opens access. If clientName plumbing is broken (gateway doesn't
    // pass clientName to hasPermission), resolution falls through to default
    // and every assertion below fails — making this a positive test of the
    // full end-to-end clientName path.
    const permissions: Permissions = {
      default: { _type: "default-block", allow: [] },
      consumers: {},
      clientNames: {
        "end-client": {
          _type: "default-allow",
          block: [],
        },
      },
    };

    let testHarness: TestHarness;
    beforeAll(async () => {
      const config = buildConfig({ permissions });
      testHarness = getTestHarness({
        config,
        clientConnectExtraHeaders: {}, // no x-lunar-consumer-tag
      });
      await testHarness.initialize(transportType);
    });

    afterAll(async () => {
      await testHarness.shutdown();
    });

    it("resolves clientNames entry when only clientName identity is available", async () => {
      const tools = await testHarness.client.listTools();
      expect(tools.tools.length).toBeGreaterThan(0);

      const echoRes = await testHarness.client.callTool({
        name: "echo-service__echo",
        arguments: { message: "hello" },
      });
      expect((echoRes.content as [{ text: string }])[0]?.text).toEqual(
        "Tool echo: hello",
      );
    });
  });
});
