import { describe, it, expect, vi, beforeEach } from "vitest";
import { Registry } from "../../src/registry/index.js";
import type { Config } from "../../src/config.js";
import type { HarnessClient } from "../../src/client/harness-client.js";
import { HarnessApiError } from "../../src/utils/errors.js";
import { registerAllTools } from "../../src/tools/index.js";

function makeConfig(overrides: Partial<Config> = {}): Config {
  return {
    HARNESS_API_KEY: "pat.test",
    HARNESS_ACCOUNT_ID: "test-account",
    HARNESS_BASE_URL: "https://app.harness.io",
    HARNESS_ORG: "default",
    HARNESS_PROJECT: "test-project",
    HARNESS_API_TIMEOUT_MS: 30000,
    HARNESS_MAX_RETRIES: 3,
    HARNESS_MAX_BODY_SIZE_MB: 10,
    HARNESS_RATE_LIMIT_RPS: 10,
    HARNESS_READ_ONLY: false,
    HARNESS_SKIP_ELICITATION: false,
    HARNESS_ALLOW_HTTP: false,
    HARNESS_FME_BASE_URL: "https://api.split.io",
    LOG_LEVEL: "info",
    ...overrides,
  };
}

function makeClient(requestFn?: (...args: unknown[]) => unknown): HarnessClient {
  return {
    request: requestFn ?? vi.fn().mockResolvedValue({}),
    account: "test-account",
  } as unknown as HarnessClient;
}

describe("Registry", () => {
  describe("constructor — toolset loading", () => {
    it("loads all toolsets when HARNESS_TOOLSETS is not set", () => {
      const registry = new Registry(makeConfig());
      const desc = registry.describe() as { total_toolsets: number };
      // There are 23 toolsets imported in registry/index.ts (including access_control)
      // But ToolsetName only has 22 entries. Let's just check it loaded many.
      expect(desc.total_toolsets).toBeGreaterThanOrEqual(20);
    });

    it("filters to specific toolsets when HARNESS_TOOLSETS is set", () => {
      const registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "pipelines,services" }));
      const desc = registry.describe() as { total_toolsets: number };
      expect(desc.total_toolsets).toBe(2);
    });

    it("throws for invalid toolset names in HARNESS_TOOLSETS", () => {
      expect(() => new Registry(makeConfig({ HARNESS_TOOLSETS: "nonexistent" }))).toThrow(
        /Invalid HARNESS_TOOLSETS: "nonexistent"/,
      );
    });

    it("throws for typo in toolset name (e.g. 'pipeline' instead of 'pipelines')", () => {
      expect(() => new Registry(makeConfig({ HARNESS_TOOLSETS: "pipeline" }))).toThrow(
        /Invalid HARNESS_TOOLSETS: "pipeline"/,
      );
      expect(() => new Registry(makeConfig({ HARNESS_TOOLSETS: "pipeline" }))).toThrow(
        /Valid toolset names:/,
      );
    });

    it("throws listing all invalid names when multiple are wrong", () => {
      expect(() => new Registry(makeConfig({ HARNESS_TOOLSETS: "pipelines,badname,services,oops" }))).toThrow(
        /Invalid HARNESS_TOOLSETS: "badname", "oops"/,
      );
    });

    it("accepts all valid toolset names without error", () => {
      // Just use a few known-good names
      const registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "pipelines,services,connectors" }));
      const desc = registry.describe() as { total_toolsets: number };
      expect(desc.total_toolsets).toBe(3);
    });

    it("accepts legacy agent-pipelines toolset name as an alias for agents", () => {
      const registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "agent-pipelines" }));
      const desc = registry.describe() as { total_toolsets: number };
      expect(desc.total_toolsets).toBe(1);
      expect(registry.getResource("agent").toolset).toBe("agents");
    });

    it("includes ai-evals in defaults", () => {
      const registry = new Registry(makeConfig());
      const res = registry.getResource("eval_dataset");
      expect(res.resourceType).toBe("eval_dataset");
    });

    it("accepts legacy agent-pipelines alias with + prefix", () => {
      const registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "+agent-pipelines" }));
      const desc = registry.describe() as { total_toolsets: number };
      expect(desc.total_toolsets).toBeGreaterThan(20);
      expect(registry.getResource("agent").toolset).toBe("agents");
    });

    it("+ prefix preserves default toolsets", () => {
      const registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "+ai-evals" }));
      // Default toolset resource still accessible
      const res = registry.getResource("pipeline");
      expect(res.resourceType).toBe("pipeline");
    });

    it("- prefix removes from defaults", () => {
      const registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "-chaos" }));
      expect(() => registry.getResource("chaos_experiment")).toThrow(/Unknown resource_type/);
      // Other defaults still present
      const res = registry.getResource("pipeline");
      expect(res.resourceType).toBe("pipeline");
    });

    it("mixed +/- syntax", () => {
      const registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "+ai-evals,-chaos" }));
      // ai-evals enabled
      const ds = registry.getResource("eval_dataset");
      expect(ds.resourceType).toBe("eval_dataset");
      // chaos removed
      expect(() => registry.getResource("chaos_experiment")).toThrow(/Unknown resource_type/);
      // other defaults still present
      const p = registry.getResource("pipeline");
      expect(p.resourceType).toBe("pipeline");
    });

    it("throws for invalid names with +/- prefix", () => {
      expect(() => new Registry(makeConfig({ HARNESS_TOOLSETS: "+badname" }))).toThrow(
        /Invalid HARNESS_TOOLSETS: "badname"/,
      );
    });

    it("explicit list can include opt-in toolsets", () => {
      const registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "pipelines,ai-evals" }));
      const desc = registry.describe() as { total_toolsets: number };
      expect(desc.total_toolsets).toBe(2);
      const ds = registry.getResource("eval_dataset");
      expect(ds.resourceType).toBe("eval_dataset");
    });

    it("registers tools when enabled toolsets have no resources for some operations", () => {
      const registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "logs" }));
      const server = {
        registerTool: vi.fn(),
      };

      expect(() => registerAllTools(
        server as never,
        registry,
        makeClient(),
        makeConfig({ HARNESS_TOOLSETS: "logs" }),
      )).not.toThrow();
      expect(server.registerTool).toHaveBeenCalled();
    });

    it("registers every tool with explicit GPT App annotation booleans", () => {
      const registry = new Registry(makeConfig());
      const server = {
        registerTool: vi.fn(),
      };

      registerAllTools(
        server as never,
        registry,
        makeClient(),
        makeConfig(),
      );

      expect(server.registerTool).toHaveBeenCalled();

      const LOCAL_ONLY_TOOLS = new Set(["harness_describe", "harness_schema"]);

      for (const [toolName, definition] of server.registerTool.mock.calls) {
        const annotations = (definition as { annotations?: Record<string, unknown> }).annotations;
        expect(annotations, `${toolName} should define annotations`).toBeDefined();
        expect(typeof annotations?.readOnlyHint, `${toolName} readOnlyHint`).toBe("boolean");
        expect(typeof annotations?.destructiveHint, `${toolName} destructiveHint`).toBe("boolean");
        expect(typeof annotations?.openWorldHint, `${toolName} openWorldHint`).toBe("boolean");

        if (LOCAL_ONLY_TOOLS.has(toolName)) {
          expect(annotations?.openWorldHint, `${toolName} is local-only`).toBe(false);
        } else {
          expect(annotations?.openWorldHint, `${toolName} calls external API`).toBe(true);
        }
      }
    });

    it("registers every tool with an outputSchema", () => {
      const registry = new Registry(makeConfig());
      const server = {
        registerTool: vi.fn(),
      };

      registerAllTools(
        server as never,
        registry,
        makeClient(),
        makeConfig(),
      );

      for (const [toolName, definition] of server.registerTool.mock.calls) {
        const outputSchema = (definition as { outputSchema?: unknown }).outputSchema;
        expect(outputSchema, `${toolName} should define outputSchema`).toBeDefined();
      }
    });
  });

  describe("getResource", () => {
    let registry: Registry;
    beforeEach(() => {
      registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "pipelines" }));
    });

    it("returns a resource definition for a valid type", () => {
      const def = registry.getResource("pipeline");
      expect(def.resourceType).toBe("pipeline");
      expect(def.displayName).toBe("Pipeline");
    });

    it("throws for unknown resource type with available list", () => {
      expect(() => registry.getResource("nonexistent")).toThrow(/Unknown resource_type "nonexistent"/);
      expect(() => registry.getResource("nonexistent")).toThrow(/Available:/);
    });
  });

  describe("getAllResourceTypes", () => {
    it("returns sorted array of resource types", () => {
      const registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "pipelines" }));
      const types = registry.getAllResourceTypes();
      expect(Array.isArray(types)).toBe(true);
      expect(types).toContain("pipeline");
      // Verify sorted
      const sorted = [...types].sort();
      expect(types).toEqual(sorted);
    });
  });

  describe("supportsOperation", () => {
    let registry: Registry;
    beforeEach(() => {
      registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "pipelines" }));
    });

    it("returns true for supported operation", () => {
      expect(registry.supportsOperation("pipeline", "list")).toBe(true);
      expect(registry.supportsOperation("pipeline", "get")).toBe(true);
    });

    it("returns false for unsupported operation", () => {
      expect(registry.supportsOperation("pipeline", "nonexistent" as any)).toBe(false);
    });

    it("returns false for unknown resource type", () => {
      expect(registry.supportsOperation("nonexistent", "list")).toBe(false);
    });
  });

  describe("describe", () => {
    it("returns structured metadata", () => {
      const registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "pipelines" }));
      const desc = registry.describe() as {
        total_resource_types: number;
        total_toolsets: number;
        toolsets: Record<string, unknown>;
      };
      expect(desc.total_toolsets).toBe(1);
      expect(desc.total_resource_types).toBeGreaterThan(0);
      expect(desc.toolsets).toHaveProperty("pipelines");
    });

    it("marks account-queryable settings resources as multi-scope", () => {
      const registry = new Registry(makeConfig({
        HARNESS_TOOLSETS: "connectors,services,environments,infrastructure,secrets,templates",
      }));

      for (const resourceType of ["connector", "service", "environment", "infrastructure", "secret", "template"]) {
        expect(registry.getResource(resourceType).supportedScopes).toEqual(["account", "org", "project"]);
      }
    });

    it("includes supportedScopes in toolset-level describe metadata", () => {
      const registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "connectors" }));
      const desc = registry.describe() as {
        toolsets: {
          connectors: {
            resources: Array<{ resource_type: string; supportedScopes?: string[] }>;
          };
        };
      };

      const connector = desc.toolsets.connectors.resources.find((r) => r.resource_type === "connector");

      expect(connector?.supportedScopes).toEqual(["account", "org", "project"]);
    });

    it("does not infer explicit resource_scope support from scopeOptional", () => {
      const registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "scs" }));
      const desc = registry.describe() as {
        toolsets: {
          scs: {
            resources: Array<{ resource_type: string; supportedScopes?: string[] }>;
          };
        };
      };

      const vulnerability = desc.toolsets.scs.resources.find((r) => r.resource_type === "scs_component_vulnerability");

      expect(vulnerability?.supportedScopes).toBeUndefined();
      expect(registry.getSupportedScopes("scs_component_vulnerability")).toEqual(["project"]);
    });
  });

  describe("getAllFilterFields", () => {
    it("returns deduplicated FilterFieldSpec objects across enabled toolsets", () => {
      const registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "pipelines,connectors" }));
      const fields = registry.getAllFilterFields();
      expect(Array.isArray(fields)).toBe(true);
      // Every entry should have name and description
      for (const f of fields) {
        expect(f).toHaveProperty("name");
        expect(f).toHaveProperty("description");
        expect(typeof f.name).toBe("string");
        expect(typeof f.description).toBe("string");
      }
      // "search_term" appears in both pipelines and connectors — should only appear once
      const searchTermEntries = fields.filter((f) => f.name === "search_term");
      expect(searchTermEntries).toHaveLength(1);
    });

    it("includes enum metadata when defined", () => {
      const registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "connectors" }));
      const fields = registry.getAllFilterFields();
      const typeField = fields.find((f) => f.name === "type");
      expect(typeField).toBeDefined();
      expect(typeField!.enum).toBeDefined();
      expect(typeField!.enum!.length).toBeGreaterThan(0);
    });

    it("returns empty array when no toolsets have filter fields", () => {
      // All toolsets have some filters, but verify the method doesn't crash with a narrow set
      const registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "pipelines" }));
      const fields = registry.getAllFilterFields();
      expect(fields.length).toBeGreaterThan(0);
    });
  });

  describe("LLM field discovery flow", () => {
    let registry: Registry;
    beforeEach(() => {
      registry = new Registry(makeConfig());
    });

    it("harness_describe exposes listFilterFields for each resource type", () => {
      // Simulate what an LLM would get from harness_describe(resource_type="pipeline")
      const def = registry.getResource("pipeline");
      expect(def.listFilterFields).toBeDefined();
      const names = def.listFilterFields!.map((f) => f.name);
      expect(names).toContain("search_term");
      expect(names).toContain("module");
    });

    it("listFilterFields are accepted by dispatch when passed as flat input", async () => {
      // Simulate: LLM discovers filter fields via describe, then passes them via filters catch-all
      const def = registry.getResource("connector");
      const names = def.listFilterFields!.map((f) => f.name);
      expect(names).toContain("search_term");
      expect(names).toContain("type");
      expect(names).toContain("category");

      const mockRequest = vi.fn().mockResolvedValue({
        data: { content: [], totalElements: 0 },
      });
      const client = makeClient(mockRequest);

      // Pass discovered filter fields as flat input (as they arrive after spreading filters catch-all)
      await registry.dispatch(client, "connector", "list", {
        search_term: "docker",
        type: "DockerRegistry",
        category: "CONNECTOR",
        page: 0,
        size: 10,
      });

      const call = mockRequest.mock.calls[0][0];
      // Connector list uses POST listV2 with body-based filtering
      expect(call.method).toBe("POST");
      expect(call.path).toBe("/ng/api/connectors/listV2");
      expect(call.params).toMatchObject({
        searchTerm: "docker",
      });
      // type and category are in the body, not query params
      expect(call.params).not.toHaveProperty("type");
      expect(call.params).not.toHaveProperty("category");
      expect(call.body).toMatchObject({
        filterType: "Connector",
        types: ["DockerRegistry"],
        categories: ["CONNECTOR"],
      });
    });

    it("connector list omits type/category from body when not provided", async () => {
      const mockRequest = vi.fn().mockResolvedValue({
        data: { content: [], totalElements: 0 },
      });
      const client = makeClient(mockRequest);

      await registry.dispatch(client, "connector", "list", {
        search_term: "aws",
        page: 0,
        size: 10,
      });

      const call = mockRequest.mock.calls[0][0];
      expect(call.method).toBe("POST");
      expect(call.body).toMatchObject({ filterType: "Connector" });
      expect(call.body.types).toBeUndefined();
      expect(call.body.categories).toBeUndefined();
    });

    it("audit_resource_type filter is serialized into the request body", async () => {
      const mockRequest = vi.fn().mockResolvedValue({
        data: { content: [], totalElements: 0 },
      });
      const client = makeClient(mockRequest);

      await registry.dispatch(client, "audit_event", "list", {
        audit_resource_type: "PIPELINE",
        action: "CREATE",
        page: 0,
        size: 20,
      });

      const call = mockRequest.mock.calls[0][0];
      expect(call.method).toBe("POST");
      expect(call.path).toBe("/audit/api/audits/list");
      expect(call.body).toMatchObject({
        filterType: "Audit",
        actions: ["CREATE"],
        resources: [{ type: "PIPELINE" }],
      });
    });

    it("audit_event list omits resources field when audit_resource_type not provided", async () => {
      const mockRequest = vi.fn().mockResolvedValue({
        data: { content: [], totalElements: 0 },
      });
      const client = makeClient(mockRequest);

      await registry.dispatch(client, "audit_event", "list", {
        action: "DELETE",
      });

      const call = mockRequest.mock.calls[0][0];
      expect(call.body.actions).toEqual(["DELETE"]);
      expect(call.body.resources).toBeUndefined();
    });

    it("identifierFields include parent IDs for nested resources", () => {
      // Trigger needs both pipeline_id and trigger_id
      const triggerDef = registry.getResource("trigger");
      expect(triggerDef.identifierFields).toContain("trigger_id");
      expect(triggerDef.identifierFields).toContain("pipeline_id");
      // pipeline_id is passed via queryParams on get, discoverable via describe
      const getSpec = triggerDef.operations.get;
      expect(getSpec?.queryParams).toHaveProperty("pipeline_id");
    });

    it("most listable resource types expose listFilterFields", () => {
      const allTypes = registry.getAllResourceTypes();
      let withFilters = 0;
      let listable = 0;
      for (const type of allTypes) {
        const def = registry.getResource(type);
        if (def.operations.list) {
          listable++;
          if (def.listFilterFields) withFilters++;
        }
      }
      // Majority of listable resources should have filter fields defined
      expect(withFilters / listable).toBeGreaterThanOrEqual(0.5);
    });

    it("describeSummary includes filter discovery hint", () => {
      const summary = registry.describeSummary() as { hint: string };
      expect(summary.hint).toContain("harness_describe");
    });
  });

  describe("dispatch", () => {
    let registry: Registry;
    beforeEach(() => {
      registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "pipelines" }));
    });

    it("builds correct path and params for a list operation", async () => {
      const mockRequest = vi.fn().mockResolvedValue({
        data: { content: [{ identifier: "p1" }], totalElements: 1 },
      });
      const client = makeClient(mockRequest);

      await registry.dispatch(client, "pipeline", "list", {
        org_id: "default",
        project_id: "test-project",
        search_term: "deploy",
        page: 0,
        size: 10,
      });

      expect(mockRequest).toHaveBeenCalledOnce();
      const call = mockRequest.mock.calls[0][0];
      expect(call.method).toBe("POST");
      expect(call.path).toBe("/pipeline/api/pipelines/list");
      expect(call.params).toMatchObject({
        orgIdentifier: "default",
        projectIdentifier: "test-project",
        searchTerm: "deploy",
        page: 0,
        size: 10,
      });
    });

    it("omits default org/project query params for explicit account-scope connector list", async () => {
      const accountRegistry = new Registry(makeConfig({ HARNESS_TOOLSETS: "connectors" }));
      const mockRequest = vi.fn().mockResolvedValue({
        data: { content: [], totalElements: 0 },
      });
      const client = makeClient(mockRequest);

      await accountRegistry.dispatch(client, "connector", "list", {
        resource_scope: "account",
        type: "Bitbucket",
        page: 0,
        size: 100,
      });

      const call = mockRequest.mock.calls[0][0];
      expect(call.path).toBe("/ng/api/connectors/listV2");
      expect(call.params.orgIdentifier).toBeUndefined();
      expect(call.params.projectIdentifier).toBeUndefined();
      expect(call.body).toMatchObject({
        filterType: "Connector",
        types: ["Bitbucket"],
      });
    });

    it("injects only org query params for explicit org-scope connector list", async () => {
      const accountRegistry = new Registry(makeConfig({ HARNESS_TOOLSETS: "connectors" }));
      const mockRequest = vi.fn().mockResolvedValue({
        data: { content: [], totalElements: 0 },
      });
      const client = makeClient(mockRequest);

      await accountRegistry.dispatch(client, "connector", "list", {
        resource_scope: "org",
        org_id: "platform",
      });

      const call = mockRequest.mock.calls[0][0];
      expect(call.params.orgIdentifier).toBe("platform");
      expect(call.params.projectIdentifier).toBeUndefined();
    });

    it("throws before dispatch when explicit org scope lacks org_id and HARNESS_ORG", async () => {
      const accountRegistry = new Registry(makeConfig({
        HARNESS_TOOLSETS: "connectors",
        HARNESS_ORG: undefined,
        HARNESS_PROJECT: undefined,
      }));
      const mockRequest = vi.fn().mockResolvedValue({
        data: { content: [], totalElements: 0 },
      });
      const client = makeClient(mockRequest);

      await expect(
        accountRegistry.dispatch(client, "connector", "list", {
          resource_scope: "org",
        }),
      ).rejects.toThrow(/resource_scope "org" requires org_id or HARNESS_ORG/);
      expect(mockRequest).not.toHaveBeenCalled();
    });

    it("throws before dispatch when explicit project scope lacks project_id and HARNESS_PROJECT", async () => {
      const accountRegistry = new Registry(makeConfig({
        HARNESS_TOOLSETS: "connectors",
        HARNESS_ORG: "platform",
        HARNESS_PROJECT: undefined,
      }));
      const mockRequest = vi.fn().mockResolvedValue({
        data: { content: [], totalElements: 0 },
      });
      const client = makeClient(mockRequest);

      await expect(
        accountRegistry.dispatch(client, "connector", "list", {
          resource_scope: "project",
        }),
      ).rejects.toThrow(/resource_scope "project" requires project_id or HARNESS_PROJECT/);
      expect(mockRequest).not.toHaveBeenCalled();
    });

    it("keeps default project scope when scope is omitted", async () => {
      const accountRegistry = new Registry(makeConfig({ HARNESS_TOOLSETS: "connectors" }));
      const mockRequest = vi.fn().mockResolvedValue({
        data: { content: [], totalElements: 0 },
      });
      const client = makeClient(mockRequest);

      await accountRegistry.dispatch(client, "connector", "list", {});

      const call = mockRequest.mock.calls[0][0];
      expect(call.params.orgIdentifier).toBe("default");
      expect(call.params.projectIdentifier).toBe("test-project");
    });

    it.each([
      ["connectors", "connector"],
      ["services", "service"],
      ["environments", "environment"],
      ["infrastructure", "infrastructure"],
      ["secrets", "secret"],
      ["templates", "template"],
    ])("supports account/org/project list scoping for %s", async (toolset, resourceType) => {
      const scopedRegistry = new Registry(makeConfig({ HARNESS_TOOLSETS: toolset }));
      const mockRequest = vi.fn().mockResolvedValue({
        data: { content: [], totalElements: 0 },
      });
      const client = makeClient(mockRequest);

      await scopedRegistry.dispatch(client, resourceType, "list", {
        resource_scope: "account",
      });
      await scopedRegistry.dispatch(client, resourceType, "list", {
        resource_scope: "org",
        org_id: "org-level",
      });
      await scopedRegistry.dispatch(client, resourceType, "list", {
        resource_scope: "project",
        org_id: "proj-org",
        project_id: "proj-level",
      });

      const accountCall = mockRequest.mock.calls[0][0];
      const orgCall = mockRequest.mock.calls[1][0];
      const projectCall = mockRequest.mock.calls[2][0];

      expect(accountCall.params.orgIdentifier).toBeUndefined();
      expect(accountCall.params.projectIdentifier).toBeUndefined();
      expect(orgCall.params.orgIdentifier).toBe("org-level");
      expect(orgCall.params.projectIdentifier).toBeUndefined();
      expect(projectCall.params.orgIdentifier).toBe("proj-org");
      expect(projectCall.params.projectIdentifier).toBe("proj-level");
    });

    it("omits default org/project query params for explicit account-scope secret get", async () => {
      const accountRegistry = new Registry(makeConfig({ HARNESS_TOOLSETS: "secrets" }));
      const mockRequest = vi.fn().mockResolvedValue({ data: { identifier: "acctSecret" } });
      const client = makeClient(mockRequest);

      await accountRegistry.dispatch(client, "secret", "get", {
        resource_scope: "account",
        secret_id: "acctSecret",
      });

      const call = mockRequest.mock.calls[0][0];
      expect(call.path).toBe("/ng/api/v2/secrets/acctSecret");
      expect(call.params.orgIdentifier).toBeUndefined();
      expect(call.params.projectIdentifier).toBeUndefined();
    });

    it("template update uses NG API path with pathParams", async () => {
      const templateRegistry = new Registry(makeConfig({
        HARNESS_TOOLSETS: "templates",
        HARNESS_ORG: "default-org",
        HARNESS_PROJECT: "default-proj",
      }));
      const mockRequest = vi.fn().mockResolvedValue({ data: {} });
      const client = makeClient(mockRequest);

      await templateRegistry.dispatch(client, "template", "update", {
        resource_scope: "project",
        template_id: "my-template",
        version_label: "v2",
        body: { template_yaml: "template:\n  name: My Template\n  type: Step\n" },
      });

      const call = mockRequest.mock.calls[0][0];
      expect(call.path).toBe("/template/api/templates/update/my-template/v2");
    });

    it("template update at account scope uses same NG API path without org/project query params", async () => {
      const templateRegistry = new Registry(makeConfig({
        HARNESS_TOOLSETS: "templates",
        HARNESS_ORG: "default-org",
        HARNESS_PROJECT: "default-proj",
      }));
      const mockRequest = vi.fn().mockResolvedValue({ data: {} });
      const client = makeClient(mockRequest);

      await templateRegistry.dispatch(client, "template", "update", {
        resource_scope: "account",
        template_id: "my-template",
        version_label: "v2",
        body: { template_yaml: "template:\n  name: My Template\n  type: Step\n" },
      });

      const call = mockRequest.mock.calls[0][0];
      expect(call.path).toBe("/template/api/templates/update/my-template/v2");
      expect(call.params.orgIdentifier).toBeUndefined();
      expect(call.params.projectIdentifier).toBeUndefined();
    });

    it("does not treat resource-specific scope filters as dispatcher scope", async () => {
      const gitopsRegistry = new Registry(makeConfig({ HARNESS_TOOLSETS: "gitops" }));
      const mockRequest = vi.fn().mockResolvedValue({
        data: { content: [], totalElements: 0 },
      });
      const client = makeClient(mockRequest);

      await gitopsRegistry.dispatch(client, "gitops_cluster_link", "list", {
        environment_id: "prod",
        scope: "ACCOUNT",
      });

      const call = mockRequest.mock.calls[0][0];
      expect(call.params).toMatchObject({
        orgIdentifier: "default",
        projectIdentifier: "test-project",
        environmentIdentifier: "prod",
        scope: "ACCOUNT",
      });
    });

    it("rejects unsupported explicit org scope for scopeOptional resources", async () => {
      const scsRegistry = new Registry(makeConfig({ HARNESS_TOOLSETS: "scs" }));
      const client = makeClient();

      await expect(
        scsRegistry.dispatch(client, "scs_component_vulnerability", "list", {
          resource_scope: "org",
          org_id: "default",
          purl: "pkg:npm/express@4.18.0",
        }),
      ).rejects.toThrow(/scs_component_vulnerability does not support org scope/);
    });

    it("builds correct path with path params for a get operation", async () => {
      const mockRequest = vi.fn().mockResolvedValue({ data: { identifier: "my-pipeline" } });
      const client = makeClient(mockRequest);

      await registry.dispatch(client, "pipeline", "get", {
        pipeline_id: "my-pipeline",
      });

      expect(mockRequest).toHaveBeenCalledOnce();
      const call = mockRequest.mock.calls[0][0];
      expect(call.method).toBe("GET");
      expect(call.path).toBe("/pipeline/api/pipelines/my-pipeline");
    });

    it("throws on unsupported operation", async () => {
      const client = makeClient();
      await expect(
        registry.dispatch(client, "pipeline", "nonexistent" as any, {}),
      ).rejects.toThrow(/does not support "nonexistent"/);
    });

    it("throws on unknown resource type", async () => {
      const client = makeClient();
      await expect(
        registry.dispatch(client, "nonexistent", "list", {}),
      ).rejects.toThrow(/Unknown resource_type "nonexistent"/);
    });

    it("throws when required path param is missing", async () => {
      const client = makeClient();
      await expect(
        registry.dispatch(client, "pipeline", "get", {}),
      ).rejects.toThrow(/Missing required field/);
    });

    it("closes a Harness Code pull request via execute action", async () => {
      const prRegistry = new Registry(makeConfig({ HARNESS_TOOLSETS: "pull-requests" }));
      const mockRequest = vi.fn().mockResolvedValue({ number: 42, state: "closed" });
      const client = makeClient(mockRequest);

      await prRegistry.dispatchExecute(client, "pull_request", "close", {
        repo_id: "my-repo",
        pr_number: "42",
      });

      expect(mockRequest).toHaveBeenCalledOnce();
      const call = mockRequest.mock.calls[0][0];
      expect(call.method).toBe("POST");
      expect(call.path).toBe("/code/api/v1/repos/my-repo/pullreq/42/state");
      expect(call.body).toEqual({ state: "closed" });
    });

    it("pipeline execute sends pipeline_branch as ?pipelineBranchName= query param", async () => {
      const mockRequest = vi.fn().mockResolvedValue({ planExecution: { uuid: "exec-123" } });
      const client = makeClient(mockRequest);

      await registry.dispatchExecute(client, "pipeline", "run", {
        pipeline_id: "my-pipeline",
        pipeline_branch: "feature/my-fix",
      });

      expect(mockRequest).toHaveBeenCalledOnce();
      const call = mockRequest.mock.calls[0][0];
      expect(call.method).toBe("POST");
      expect(call.path).toContain("/pipeline/api/pipeline/execute/my-pipeline");
      expect(call.params).toMatchObject({ pipelineBranchName: "feature/my-fix" });
    });

    it("pipeline execute omits pipelineBranchName when pipeline_branch not provided", async () => {
      const mockRequest = vi.fn().mockResolvedValue({ planExecution: { uuid: "exec-456" } });
      const client = makeClient(mockRequest);

      await registry.dispatchExecute(client, "pipeline", "run", {
        pipeline_id: "my-pipeline",
      });

      expect(mockRequest).toHaveBeenCalledOnce();
      const call = mockRequest.mock.calls[0][0];
      expect(call.params?.pipelineBranchName).toBeUndefined();
    });

    it("pipeline execute omits pipelineBranchName when pipeline_branch is empty string", async () => {
      const mockRequest = vi.fn().mockResolvedValue({ planExecution: { uuid: "exec-789" } });
      const client = makeClient(mockRequest);

      await registry.dispatchExecute(client, "pipeline", "run", {
        pipeline_id: "my-pipeline",
        pipeline_branch: "",
      });

      expect(mockRequest).toHaveBeenCalledOnce();
      const call = mockRequest.mock.calls[0][0];
      expect(call.params?.pipelineBranchName).toBeUndefined();
    });

    it("pipeline execute sends both pipeline_branch and inputs.branch independently", async () => {
      const mockRequest = vi.fn().mockResolvedValue({ planExecution: { uuid: "exec-abc" } });
      const client = makeClient(mockRequest);

      await registry.dispatchExecute(client, "pipeline", "run", {
        pipeline_id: "my-pipeline",
        pipeline_branch: "feature/my-fix",
        inputs: { branch: "feature/my-fix" },
      });

      expect(mockRequest).toHaveBeenCalledOnce();
      const call = mockRequest.mock.calls[0][0];
      // pipeline_branch → URL query param (which git branch loads the pipeline YAML)
      expect(call.params).toMatchObject({ pipelineBranchName: "feature/my-fix" });
      // inputs.branch → body build structure (which code branch the CI job checks out)
      expect(JSON.stringify(call.body)).toContain("feature/my-fix");
    });

    it("pipeline update with yamlPipeline sends raw YAML string as body with Content-Type header and returns openInHarness", async () => {
      const yaml = "pipeline:\n  name: Test\n  identifier: test_pipeline\n  stages: []";
      const mockRequest = vi.fn().mockResolvedValue({
        data: { identifier: "test_pipeline", yamlPipeline: yaml },
      });
      const client = makeClient(mockRequest);

      const result = (await registry.dispatch(client, "pipeline", "update", {
        pipeline_id: "test_pipeline",
        project_id: "my-project",
        org_id: "default",
        body: { yamlPipeline: yaml },
      })) as Record<string, unknown>;

      expect(mockRequest).toHaveBeenCalledOnce();
      const call = mockRequest.mock.calls[0][0];
      expect(call.method).toBe("PUT");
      expect(call.path).toBe("/pipeline/api/pipelines/v2/test_pipeline");
      // Body is the raw YAML string, not a JSON wrapper
      expect(call.body).toBe(yaml);
      // Content-Type header is set to application/yaml via spec headers
      expect(call.headers).toEqual({ "Content-Type": "application/yaml" });
      expect(result.openInHarness).toBeDefined();
      expect(String(result.openInHarness)).toContain("/pipelines/test_pipeline/pipeline-studio");
    });

    it("pipeline update with body.pipeline passes body through", async () => {
      const body = { pipeline: { name: "X", identifier: "x", stages: [] } };
      const mockRequest = vi.fn().mockResolvedValue({ data: body.pipeline });
      const client = makeClient(mockRequest);

      await registry.dispatch(client, "pipeline", "update", {
        pipeline_id: "x",
        project_id: "p",
        org_id: "default",
        body,
      });

      const call = mockRequest.mock.calls[0][0];
      expect(call.body).toEqual(body);
    });

    it("pipeline update without pipeline or yamlPipeline throws", async () => {
      const client = makeClient();
      await expect(
        registry.dispatch(client, "pipeline", "update", {
          pipeline_id: "x",
          body: {},
        }),
      ).rejects.toThrow(/body must be a YAML string/);
    });
  });

  describe("template global get", () => {
    it("overrides accountIdentifier for global template fetch", async () => {
      const mockRequest = vi.fn().mockResolvedValue({ data: { identifier: "helmDeployAction" } });
      const client = makeClient(mockRequest);
      const registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "templates" }));

      await registry.dispatch(client, "template", "get", {
        template_id: "helmDeployAction",
        version_label: "1.0.8",
        account_id: "__GLOBAL_TEMPLATES_ACCOUNT_ID__",
      });

      const call = mockRequest.mock.calls[0][0];
      expect(call.method).toBe("GET");
      expect(call.path).toBe("/template/api/templates/helmDeployAction");
      expect(call.params).toMatchObject({
        accountIdentifier: "__GLOBAL_TEMPLATES_ACCOUNT_ID__",
        versionLabel: "1.0.8",
      });
    });
  });

  describe("cost category create — account body injection", () => {
    let registry: Registry;

    it("injects accountId into the request body", async () => {
      registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "ccm" }));
      const mockRequest = vi.fn().mockResolvedValue({ data: { uuid: "category-1" } });
      const client = makeClient(mockRequest);

      await registry.dispatch(client, "cost_category", "create", {
        body: {
          name: "Engineering",
          costTargets: [
            {
              name: "Development",
              rules: [
                {
                  viewConditions: [
                    {
                      type: "VIEW_ID_CONDITION",
                      viewField: {
                        fieldId: "labels.value",
                        fieldName: "env",
                        identifierName: "Label V2",
                        identifier: "LABEL_V2",
                      },
                      viewOperator: "IN",
                      values: ["dev"],
                    },
                  ],
                },
              ],
            },
          ],
        },
      });

      expect(mockRequest).toHaveBeenCalledOnce();
      const call = mockRequest.mock.calls[0][0];
      expect(call.method).toBe("POST");
      expect(call.path).toBe("/ccm/api/business-mapping");
      expect(call.body).toMatchObject({
        accountId: "test-account",
        name: "Engineering",
      });
      expect(call.body.accountIdentifier).toBeUndefined();
    });

    it("uses the resolved account ID for body injection", async () => {
      registry = new Registry(makeConfig({ HARNESS_ACCOUNT_ID: "internal", HARNESS_TOOLSETS: "ccm" }), {
        accountIdResolver: () => "resolved-account",
      });
      const mockRequest = vi.fn().mockResolvedValue({ data: { uuid: "category-1" } });
      const client = makeClient(mockRequest);

      await registry.dispatch(client, "cost_category", "create", {
        body: {
          name: "Engineering",
          costTargets: [{ name: "Development", rules: [] }],
        },
      });

      const call = mockRequest.mock.calls[0][0];
      expect(call.body.accountId).toBe("resolved-account");
      expect(call.body.accountId).not.toBe("internal");
    });
  });

  describe("resolved account ID propagation", () => {
    it("passes the resolved account ID to pathBuilder and deep links", async () => {
      const mockRequest = vi.fn().mockResolvedValue({
        data: { registries: [{ identifier: "reg1", name: "reg1" }] },
      });
      const client = makeClient(mockRequest);
      const registry = new Registry(
        makeConfig({ HARNESS_TOOLSETS: "registries", HARNESS_ACCOUNT_ID: "static-account" }),
        { accountIdResolver: () => "resolved-account" },
      );

      const result = (await registry.dispatch(client, "registry", "list", {
        org_id: "org1",
        project_id: "proj1",
      })) as { items: Array<Record<string, unknown>> };

      const call = mockRequest.mock.calls[0][0];
      expect(call.path).toBe("/har/api/v1/spaces/resolved-account/org1/proj1/+/registries");
      expect(result.items[0].openInHarness).toContain("/ng/account/resolved-account/");
    });

    it("uses the resolved account ID for custom account scope params", async () => {
      const mockRequest = vi.fn().mockResolvedValue({ data: [] });
      const client = makeClient(mockRequest);
      const registry = new Registry(
        makeConfig({ HARNESS_TOOLSETS: "sto", HARNESS_ACCOUNT_ID: "static-account" }),
        { accountIdResolver: () => "resolved-account" },
      );

      await registry.dispatch(client, "security_issue", "list", {
        org_id: "org1",
        project_id: "proj1",
      });

      const call = mockRequest.mock.calls[0][0];
      expect(call.params.accountId).toBe("resolved-account");
    });

    it("uses the resolved account ID when injecting accountIdentifier into request bodies", async () => {
      const mockRequest = vi.fn().mockResolvedValue({ applications: [] });
      const client = makeClient(mockRequest);
      const registry = new Registry(
        makeConfig({ HARNESS_TOOLSETS: "gitops", HARNESS_ACCOUNT_ID: "static-account" }),
        { accountIdResolver: () => "resolved-account" },
      );

      await registry.dispatch(client, "gitops_application", "list", {
        org_id: "org1",
        project_id: "proj1",
      });

      const call = mockRequest.mock.calls[0][0];
      expect(call.body.accountIdentifier).toBe("resolved-account");
    });
  });

  describe("read-only mode", () => {
    let registry: Registry;
    beforeEach(() => {
      registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "pipelines", HARNESS_READ_ONLY: true }));
    });

    it("allows list operations", async () => {
      const mockRequest = vi.fn().mockResolvedValue({ data: { content: [], totalElements: 0 } });
      const client = makeClient(mockRequest);
      await registry.dispatch(client, "pipeline", "list", {});
      expect(mockRequest).toHaveBeenCalledOnce();
    });

    it("allows get operations", async () => {
      const mockRequest = vi.fn().mockResolvedValue({ data: { identifier: "p1" } });
      const client = makeClient(mockRequest);
      await registry.dispatch(client, "pipeline", "get", { pipeline_id: "p1" });
      expect(mockRequest).toHaveBeenCalledOnce();
    });

    it("blocks create operations", async () => {
      const client = makeClient();
      await expect(
        registry.dispatch(client, "pipeline", "create", { body: {} }),
      ).rejects.toThrow(/Read-only mode/);
    });

    it("blocks update operations", async () => {
      const client = makeClient();
      await expect(
        registry.dispatch(client, "pipeline", "update", { pipeline_id: "p1", body: {} }),
      ).rejects.toThrow(/Read-only mode/);
    });

    it("blocks delete operations", async () => {
      const client = makeClient();
      await expect(
        registry.dispatch(client, "pipeline", "delete", { pipeline_id: "p1" }),
      ).rejects.toThrow(/Read-only mode/);
    });

    it("blocks execute actions", async () => {
      const client = makeClient();
      await expect(
        registry.dispatchExecute(client, "pipeline", "run", { pipeline_id: "p1" }),
      ).rejects.toThrow(/Read-only mode/);
    });
  });

  describe("trigger pipelineIdentifier extraction", () => {
    let registry: Registry;

    beforeEach(() => {
      registry = new Registry(makeConfig());
    });

    it("flat body: extracts pipelineIdentifier and wraps in trigger envelope", async () => {
      const mockRequest = vi.fn().mockResolvedValue({ status: "SUCCESS", data: {} });
      const client = makeClient(mockRequest);
      await registry.dispatch(client, "trigger", "create", {
        pipeline_id: undefined,
        body: { name: "my-trigger", identifier: "myTrigger", pipelineIdentifier: "myPipeline", source: { type: "Scheduled" } },
      });
      const call = mockRequest.mock.calls[0][0];
      expect(call.params.targetIdentifier).toBe("myPipeline");
      const yamlBody = call.body as string;
      expect(yamlBody).toContain("trigger:");
      expect(yamlBody).toContain("pipelineIdentifier: myPipeline");
    });

    it("wrapped body: extracts pipelineIdentifier from inside trigger", async () => {
      const mockRequest = vi.fn().mockResolvedValue({ status: "SUCCESS", data: {} });
      const client = makeClient(mockRequest);
      await registry.dispatch(client, "trigger", "create", {
        pipeline_id: undefined,
        body: { trigger: { name: "my-trigger", identifier: "myTrigger", pipelineIdentifier: "myPipeline", source: { type: "Scheduled" } } },
      });
      const call = mockRequest.mock.calls[0][0];
      expect(call.params.targetIdentifier).toBe("myPipeline");
      const yamlBody = call.body as string;
      expect(yamlBody).toContain("pipelineIdentifier: myPipeline");
    });

    it("sibling body: merges pipelineIdentifier into trigger and drops from root", async () => {
      const mockRequest = vi.fn().mockResolvedValue({ status: "SUCCESS", data: {} });
      const client = makeClient(mockRequest);
      await registry.dispatch(client, "trigger", "create", {
        pipeline_id: undefined,
        body: { trigger: { name: "my-trigger", identifier: "myTrigger", source: { type: "Scheduled" } }, pipelineIdentifier: "myPipeline" },
      });
      const call = mockRequest.mock.calls[0][0];
      expect(call.params.targetIdentifier).toBe("myPipeline");
      const yamlBody = call.body as string;
      expect(yamlBody).toContain("pipelineIdentifier: myPipeline");
      // pipelineIdentifier must be inside trigger, not duplicated at root
      const lines = yamlBody.split("\n").filter((l: string) => l.includes("pipelineIdentifier"));
      expect(lines.length).toBe(1);
    });

    it("sibling body: does not overwrite existing inner pipelineIdentifier", async () => {
      const mockRequest = vi.fn().mockResolvedValue({ status: "SUCCESS", data: {} });
      const client = makeClient(mockRequest);
      await registry.dispatch(client, "trigger", "create", {
        pipeline_id: undefined,
        body: {
          trigger: { name: "t", identifier: "t", pipelineIdentifier: "innerPipeline", source: { type: "Scheduled" } },
          pipelineIdentifier: "outerPipeline",
        },
      });
      const call = mockRequest.mock.calls[0][0];
      // inner takes precedence for the query param
      expect(call.params.targetIdentifier).toBe("innerPipeline");
      const yamlBody = call.body as string;
      expect(yamlBody).toContain("pipelineIdentifier: innerPipeline");
    });

    it("explicit pipeline_id takes precedence over body extraction", async () => {
      const mockRequest = vi.fn().mockResolvedValue({ status: "SUCCESS", data: {} });
      const client = makeClient(mockRequest);
      await registry.dispatch(client, "trigger", "create", {
        pipeline_id: "explicitPipeline",
        body: { trigger: { name: "t", identifier: "t", source: { type: "Scheduled" } }, pipelineIdentifier: "bodyPipeline" },
      });
      const call = mockRequest.mock.calls[0][0];
      expect(call.params.targetIdentifier).toBe("explicitPipeline");
    });

    it("list without pipeline_id throws a clear error", async () => {
      const client = makeClient();
      await expect(
        registry.dispatch(client, "trigger", "list", {}),
      ).rejects.toThrow(/Missing required filter.*pipeline_id/);
    });
  });

  describe("bodySchema enforcement", () => {
    let registry: Registry;
    beforeEach(() => {
      registry = new Registry(makeConfig());
    });

    it("every create operation has a bodySchema", () => {
      const missing: string[] = [];
      for (const type of registry.getAllResourceTypes()) {
        const def = registry.getResource(type);
        if (def.operations.create && !def.operations.create.bodySchema) {
          missing.push(`${type}.create`);
        }
      }
      expect(missing, `Missing bodySchema on create: ${missing.join(", ")}`).toEqual([]);
    });

    it("every update operation has a bodySchema", () => {
      const missing: string[] = [];
      for (const type of registry.getAllResourceTypes()) {
        const def = registry.getResource(type);
        if (def.operations.update && !def.operations.update.bodySchema) {
          missing.push(`${type}.update`);
        }
      }
      expect(missing, `Missing bodySchema on update: ${missing.join(", ")}`).toEqual([]);
    });

    it("every executeAction with a bodyBuilder has a bodySchema", () => {
      const missing: string[] = [];
      for (const type of registry.getAllResourceTypes()) {
        const def = registry.getResource(type);
        if (def.executeActions) {
          for (const [action, spec] of Object.entries(def.executeActions)) {
            // Only require bodySchema when the action accepts a body
            if (spec.bodyBuilder && !spec.bodySchema) {
              missing.push(`${type}.${action}`);
            }
          }
        }
      }
      expect(missing, `Missing bodySchema on executeActions: ${missing.join(", ")}`).toEqual([]);
    });

    it("bodySchema fields have required properties", () => {
      const invalid: string[] = [];
      for (const type of registry.getAllResourceTypes()) {
        const def = registry.getResource(type);
        const specs = [
          ...Object.entries(def.operations).map(([op, s]) => [`${type}.${op}`, s] as const),
          ...Object.entries(def.executeActions ?? {}).map(([a, s]) => [`${type}.${a}`, s] as const),
        ];
        for (const [label, spec] of specs) {
          if (spec.bodySchema) {
            if (!spec.bodySchema.description) invalid.push(`${label}: missing description`);
            if (!Array.isArray(spec.bodySchema.fields)) invalid.push(`${label}: fields not array`);
            for (const field of spec.bodySchema.fields) {
              if (!field.name || !field.type || typeof field.required !== "boolean" || !field.description) {
                invalid.push(`${label}.${field.name ?? "?"}: incomplete field spec`);
              }
            }
          }
        }
      }
      expect(invalid, `Invalid bodySchema fields: ${invalid.join("; ")}`).toEqual([]);
    });
  });

  describe("ELK→Mongo fallback", () => {
    let registry: Registry;
    beforeEach(() => {
      registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "scs" }));
    });

    it("ELK succeeds — sends enforce_elasticsearch=true", async () => {
      const mockRequest = vi.fn().mockResolvedValue([{ id: "comp1" }]);
      const client = makeClient(mockRequest);

      await registry.dispatch(client, "artifact_security", "list", {
        source_id: "src1",
      });

      expect(mockRequest).toHaveBeenCalledOnce();
      const call = mockRequest.mock.calls[0][0];
      expect(call.params.enforce_elasticsearch).toBe("true");
    });

    it("ELK fails with 500 — falls back to Mongo", async () => {
      const mockRequest = vi.fn()
        .mockRejectedValueOnce(new HarnessApiError("ES down", 500))
        .mockResolvedValueOnce([{ id: "comp1" }]);
      const client = makeClient(mockRequest);

      await registry.dispatch(client, "artifact_security", "list", {
        source_id: "src1",
      });

      expect(mockRequest).toHaveBeenCalledTimes(2);
      expect(mockRequest.mock.calls[0][0].params.enforce_elasticsearch).toBe("true");
      expect(mockRequest.mock.calls[1][0].params.enforce_elasticsearch).toBe("false");
    });

    it("ELK fails with 404 — falls back to Mongo (index not found)", async () => {
      const mockRequest = vi.fn()
        .mockRejectedValueOnce(new HarnessApiError("Not Found", 404))
        .mockResolvedValueOnce([{ id: "comp1" }]);
      const client = makeClient(mockRequest);

      await registry.dispatch(client, "artifact_security", "list", {
        source_id: "src1",
      });

      expect(mockRequest).toHaveBeenCalledTimes(2);
      expect(mockRequest.mock.calls[1][0].params.enforce_elasticsearch).toBe("false");
    });

    it("ELK fails with 401 — throws immediately, no fallback", async () => {
      const mockRequest = vi.fn()
        .mockRejectedValueOnce(new HarnessApiError("Unauthorized", 401));
      const client = makeClient(mockRequest);

      await expect(
        registry.dispatch(client, "artifact_security", "list", { source_id: "src1" }),
      ).rejects.toThrow("Unauthorized");
      expect(mockRequest).toHaveBeenCalledOnce();
    });

    it("ELK fails with 403 — throws immediately, no fallback", async () => {
      const mockRequest = vi.fn()
        .mockRejectedValueOnce(new HarnessApiError("Forbidden", 403));
      const client = makeClient(mockRequest);

      await expect(
        registry.dispatch(client, "artifact_security", "list", { source_id: "src1" }),
      ).rejects.toThrow("Forbidden");
      expect(mockRequest).toHaveBeenCalledOnce();
    });

    it("ELK fails with non-HarnessApiError — throws immediately, no fallback", async () => {
      const mockRequest = vi.fn()
        .mockRejectedValueOnce(new Error("Network error"));
      const client = makeClient(mockRequest);

      await expect(
        registry.dispatch(client, "artifact_security", "list", { source_id: "src1" }),
      ).rejects.toThrow("Network error");
      expect(mockRequest).toHaveBeenCalledOnce();
    });

    it("both ELK and Mongo fail — Mongo error propagates", async () => {
      const mockRequest = vi.fn()
        .mockRejectedValueOnce(new HarnessApiError("ES down", 500))
        .mockRejectedValueOnce(new HarnessApiError("Mongo also down", 503));
      const client = makeClient(mockRequest);

      await expect(
        registry.dispatch(client, "artifact_security", "list", { source_id: "src1" }),
      ).rejects.toThrow("Mongo also down");
      expect(mockRequest).toHaveBeenCalledTimes(2);
    });

    it("non-elkFallback endpoint does not add enforce_elasticsearch", async () => {
      const mockRequest = vi.fn().mockResolvedValue({ data: { content: [], totalElements: 0 } });
      const client = makeClient(mockRequest);
      const pipelineRegistry = new Registry(makeConfig({ HARNESS_TOOLSETS: "pipelines" }));

      await pipelineRegistry.dispatch(client, "pipeline", "list", {});

      const call = mockRequest.mock.calls[0][0];
      expect(call.params.enforce_elasticsearch).toBeUndefined();
    });
  });

  describe("injectOrgQueryFallback (account-scoped project CRUD)", () => {
    it("falls back to config HARNESS_ORG when org_id is omitted", async () => {
      const mockRequest = vi.fn().mockResolvedValue({ data: { project: {} } });
      const client = makeClient(mockRequest);
      const registry = new Registry(
        makeConfig({ HARNESS_TOOLSETS: "platform", HARNESS_ORG: "cfg-default-org" }),
      );

      await registry.dispatch(client, "project", "get", {
        project_id: "my-proj",
      });

      const call = mockRequest.mock.calls[0][0];
      expect(call.params).toMatchObject({ orgIdentifier: "cfg-default-org" });
    });

    it("uses explicit org_id over config", async () => {
      const mockRequest = vi.fn().mockResolvedValue({ data: { project: {} } });
      const client = makeClient(mockRequest);
      const registry = new Registry(
        makeConfig({ HARNESS_TOOLSETS: "platform", HARNESS_ORG: "cfg-default-org" }),
      );

      await registry.dispatch(client, "project", "get", {
        project_id: "my-proj",
        org_id: "explicit-org",
      });

      const call = mockRequest.mock.calls[0][0];
      expect(call.params).toMatchObject({ orgIdentifier: "explicit-org" });
    });

    it("does not inject org via fallback when injectOrgQueryFallback is not set (project list)", async () => {
      const mockRequest = vi.fn().mockResolvedValue({
        data: { content: [], totalElements: 0 },
      });
      const client = makeClient(mockRequest);
      const registry = new Registry(
        makeConfig({ HARNESS_TOOLSETS: "platform", HARNESS_ORG: "cfg-default-org" }),
      );

      await registry.dispatch(client, "project", "list", {
        page: 0,
        size: 10,
      });

      const call = mockRequest.mock.calls[0][0];
      expect(call.params?.orgIdentifier).toBeUndefined();
    });
  });

  describe("methodBuilder and input-aware responseExtractor", () => {
    let registry: Registry;
    beforeEach(() => {
      registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "ccm" }));
    });

    it("methodBuilder switches to GET for cost_filter_value with value_type=business_mapping", async () => {
      const mockRequest = vi.fn().mockResolvedValue({
        resource: {
          businessMappings: [
            { uuid: "abc", name: "Environments" },
            { uuid: "def", name: "Teams" },
          ],
        },
      });
      const client = makeClient(mockRequest);

      await registry.dispatch(client, "cost_filter_value", "list", {
        value_type: "business_mapping",
      });

      const call = mockRequest.mock.calls[0][0];
      expect(call.method).toBe("GET");
      expect(call.path).toBe("/ccm/api/business-mapping");
    });

    it("methodBuilder uses POST for cost_filter_value with non-business_mapping value_type", async () => {
      const mockRequest = vi.fn().mockResolvedValue({
        data: { perspectiveFilters: [{ values: ["us-east-1", "us-west-2"] }] },
      });
      const client = makeClient(mockRequest);

      await registry.dispatch(client, "cost_filter_value", "list", {
        value_type: "region",
        time_filter: "LAST_30_DAYS",
      });

      const call = mockRequest.mock.calls[0][0];
      expect(call.method).toBe("POST");
      expect(call.path).toBe("/ccm/api/graphql");
    });

    it("responseExtractor extracts category names from business_mapping list (resource envelope)", async () => {
      const mockRequest = vi.fn().mockResolvedValue({
        resource: {
          businessMappings: [
            { uuid: "abc", name: "Environments" },
            { uuid: "def", name: "Teams" },
          ],
        },
      });
      const client = makeClient(mockRequest);

      const result = await registry.dispatch(client, "cost_filter_value", "list", {
        value_type: "business_mapping",
      });

      expect(result).toEqual({ values: ["Environments", "Teams"] });
    });

    it("responseExtractor extracts category names from business_mapping list (data envelope)", async () => {
      const mockRequest = vi.fn().mockResolvedValue({
        data: {
          businessMappings: [
            { uuid: "abc", name: "Environments" },
            { uuid: "def", name: "Teams" },
          ],
        },
      });
      const client = makeClient(mockRequest);

      const result = await registry.dispatch(client, "cost_filter_value", "list", {
        value_type: "business_mapping",
      });

      expect(result).toEqual({ values: ["Environments", "Teams"] });
    });

    it("responseExtractor extracts bucket names from single category (resource envelope)", async () => {
      const mockRequest = vi.fn().mockResolvedValue({
        resource: {
          uuid: "abc",
          name: "Environments",
          costTargets: [{ name: "Production" }, { name: "Staging" }, { name: "Dev" }],
        },
      });
      const client = makeClient(mockRequest);

      const result = await registry.dispatch(client, "cost_filter_value", "list", {
        value_type: "business_mapping",
        value_sub_type: "abc",
      });

      expect(result).toEqual({ values: ["Production", "Staging", "Dev"] });
    });

    it("responseExtractor extracts bucket names from single category (data envelope)", async () => {
      const mockRequest = vi.fn().mockResolvedValue({
        data: {
          uuid: "abc",
          name: "Environments",
          costTargets: [{ name: "Production" }, { name: "Staging" }],
        },
      });
      const client = makeClient(mockRequest);

      const result = await registry.dispatch(client, "cost_filter_value", "list", {
        value_type: "business_mapping",
        value_sub_type: "abc",
      });

      expect(result).toEqual({ values: ["Production", "Staging"] });
    });

    it("responseExtractor returns error marker when business_mapping response has no costTargets", async () => {
      const mockRequest = vi.fn().mockResolvedValue({
        resource: { uuid: "abc", name: "Empty" },
      });
      const client = makeClient(mockRequest);

      const result = await registry.dispatch(client, "cost_filter_value", "list", {
        value_type: "business_mapping",
        value_sub_type: "abc",
      });

      expect(result).toHaveProperty("_error");
      expect((result as { values: string[] }).values).toEqual([]);
    });

    it("pathBuilder routes to /ccm/api/business-mapping/{id} for value_sub_type", async () => {
      const mockRequest = vi.fn().mockResolvedValue({
        resource: {
          costTargets: [{ name: "Bucket1" }],
        },
      });
      const client = makeClient(mockRequest);

      await registry.dispatch(client, "cost_filter_value", "list", {
        value_type: "business_mapping",
        value_sub_type: "cat-uuid-123",
      });

      const call = mockRequest.mock.calls[0][0];
      expect(call.path).toBe("/ccm/api/business-mapping/cat-uuid-123");
    });
  });

});
