/**
 * Structural validation of all toolset definitions.
 *
 * Validates path/param consistency, bodySchema presence on write ops,
 * and general correctness across all 60+ resource types.
 */
import { describe, it, expect } from "vitest";
import { Registry, ALL_TOOLSET_NAMES } from "../../src/registry/index.js";
import type { Config } from "../../src/config.js";
import type { EndpointSpec, ResourceDefinition } from "../../src/registry/types.js";

function makeConfig(): Config {
  return {
    HARNESS_API_KEY: "pat.test.abc.xyz",
    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,
    LOG_LEVEL: "info",
  };
}

/** Extract {placeholders} from a path template. */
function extractPathPlaceholders(path: string): string[] {
  const matches = path.match(/\{([^}]+)\}/g);
  return matches ? matches.map((m) => m.slice(1, -1)) : [];
}

describe("Toolset structural validation", () => {
  const registry = new Registry(makeConfig());
  const allTypes = registry.getAllResourceTypes();

  /** Defaults plus every opt-in toolset — used for universal write-metadata checks. */
  const fullRegistryV0 = new Registry({
    ...makeConfig(),
    HARNESS_TOOLSETS: ALL_TOOLSET_NAMES.map((n) => `+${n}`).join(","),
    HARNESS_PIPELINE_VERSION: "0",
  } as Config);
  const fullRegistryV1 = new Registry({
    ...makeConfig(),
    HARNESS_TOOLSETS: ALL_TOOLSET_NAMES.map((n) => `+${n}`).join(","),
    HARNESS_PIPELINE_VERSION: "1",
  } as Config);

  // Union of both pipeline versions — ensures both pipeline and pipeline_v1 are covered
  const allFullTypes = [
    ...new Set([...fullRegistryV0.getAllResourceTypes(), ...fullRegistryV1.getAllResourceTypes()]),
  ];

  /** Get resource def from whichever full registry includes this type (pipeline version split). */
  function getFullResource(type: string): ResourceDefinition {
    try {
      return fullRegistryV0.getResource(type);
    } catch {
      return fullRegistryV1.getResource(type);
    }
  }

  describe("path/param consistency", () => {
    it("every path placeholder has a matching pathParams entry", () => {
      const issues: string[] = [];

      for (const type of allTypes) {
        const def = registry.getResource(type);
        const allSpecs: [string, EndpointSpec][] = [
          ...Object.entries(def.operations) as [string, EndpointSpec][],
          ...Object.entries(def.executeActions ?? {}) as [string, EndpointSpec][],
        ];

        for (const [opName, spec] of allSpecs) {
          // pathBuilder dynamically constructs the URL — static path placeholders don't apply
          if (spec.pathBuilder) continue;

          const placeholders = extractPathPlaceholders(spec.path);
          if (placeholders.length === 0) continue;

          // accountId is injected automatically, not via pathParams
          const nonAccountPlaceholders = placeholders.filter((p) => p !== "accountId");

          for (const placeholder of nonAccountPlaceholders) {
            const pathParams = spec.pathParams ?? {};
            const paramValues = Object.values(pathParams);
            if (!paramValues.includes(placeholder)) {
              // Check if it could be a scope param (org/project) handled by the
              // registry dispatch layer directly for some v1 APIs
              const isScopeParam = placeholder === "org" || placeholder === "project";
              if (!isScopeParam) {
                issues.push(`${type}.${opName}: path placeholder {${placeholder}} has no pathParams mapping`);
              }
            }
          }
        }
      }

      expect(issues, `Path/param mismatches:\n${issues.join("\n")}`).toEqual([]);
    });

    it("every pathParams value corresponds to a path placeholder", () => {
      const issues: string[] = [];

      for (const type of allTypes) {
        const def = registry.getResource(type);
        const allSpecs: [string, EndpointSpec][] = [
          ...Object.entries(def.operations) as [string, EndpointSpec][],
          ...Object.entries(def.executeActions ?? {}) as [string, EndpointSpec][],
        ];

        for (const [opName, spec] of allSpecs) {
          if (!spec.pathParams) continue;
          // When pathBuilder is set, path is not used for URL construction —
          // pathParams are only retained for deep link resolution.
          if (spec.pathBuilder) continue;
          const placeholders = new Set(extractPathPlaceholders(spec.path));

          for (const [inputKey, pathPlaceholder] of Object.entries(spec.pathParams)) {
            if (!placeholders.has(pathPlaceholder)) {
              issues.push(
                `${type}.${opName}: pathParams["${inputKey}"] = "${pathPlaceholder}" but {${pathPlaceholder}} not found in path "${spec.path}"`,
              );
            }
          }
        }
      }

      expect(issues, `Dangling pathParams:\n${issues.join("\n")}`).toEqual([]);
    });

    it("pathParams keys use snake_case (matching tool input conventions)", () => {
      const issues: string[] = [];

      for (const type of allTypes) {
        const def = registry.getResource(type);
        const allSpecs: [string, EndpointSpec][] = [
          ...Object.entries(def.operations) as [string, EndpointSpec][],
          ...Object.entries(def.executeActions ?? {}) as [string, EndpointSpec][],
        ];

        for (const [opName, spec] of allSpecs) {
          if (!spec.pathParams) continue;
          for (const inputKey of Object.keys(spec.pathParams)) {
            if (inputKey !== inputKey.toLowerCase() || inputKey.includes("-")) {
              issues.push(`${type}.${opName}: pathParams key "${inputKey}" is not snake_case`);
            }
          }
        }
      }

      expect(issues, `Non-snake_case pathParams:\n${issues.join("\n")}`).toEqual([]);
    });
  });

  describe("identifierFields consistency", () => {
    it("every resource type has identifierFields defined", () => {
      const missing: string[] = [];
      for (const type of allTypes) {
        const def = registry.getResource(type);
        if (!def.identifierFields) {
          missing.push(type);
        }
      }
      expect(missing, `Missing identifierFields array: ${missing.join(", ")}`).toEqual([]);
    });

    it("most resource types with a get operation have at least one identifierField", () => {
      const issues: string[] = [];
      for (const type of allTypes) {
        const def = registry.getResource(type);
        // Only check resources that have a get operation — they need an ID to fetch
        if (def.operations.get && def.identifierFields.length === 0) {
          issues.push(type);
        }
      }
      // Allow some dashboard/analytics types that use body-based get
      expect(issues.length).toBeLessThan(allTypes.length * 0.3);
    });

    it("identifierFields referenced in get pathParams exist", () => {
      const issues: string[] = [];
      for (const type of allTypes) {
        const def = registry.getResource(type);
        const getSpec = def.operations.get;
        if (!getSpec?.pathParams) continue;

        // The primary identifier field should be in pathParams
        const primaryField = def.identifierFields[0];
        if (primaryField && !getSpec.pathParams[primaryField]) {
          // Check queryParams as fallback (some resources use query params for IDs)
          if (!getSpec.queryParams?.[primaryField]) {
            issues.push(`${type}: primary identifierField "${primaryField}" not in get.pathParams or get.queryParams`);
          }
        }
      }
      expect(issues, `identifierField/pathParam mismatches:\n${issues.join("\n")}`).toEqual([]);
    });
  });

  describe("scope consistency", () => {
    it("every resource has a valid scope", () => {
      const validScopes = new Set(["project", "org", "account"]);
      const invalid: string[] = [];
      for (const type of allTypes) {
        const def = registry.getResource(type);
        if (!validScopes.has(def.scope)) {
          invalid.push(`${type}: scope="${def.scope}"`);
        }
      }
      expect(invalid).toEqual([]);
    });
  });

  describe("HTTP method conventions", () => {
    it("list operations use GET or POST", () => {
      const issues: string[] = [];
      for (const type of allTypes) {
        const def = registry.getResource(type);
        const listSpec = def.operations.list;
        if (listSpec && listSpec.method !== "GET" && listSpec.method !== "POST") {
          issues.push(`${type}.list: unexpected method ${listSpec.method}`);
        }
      }
      expect(issues).toEqual([]);
    });

    it("get operations use GET or POST (some analytics APIs use POST)", () => {
      const issues: string[] = [];
      for (const type of allTypes) {
        const def = registry.getResource(type);
        const getSpec = def.operations.get;
        if (getSpec && getSpec.method !== "GET" && getSpec.method !== "POST") {
          issues.push(`${type}.get: unexpected method ${getSpec.method}`);
        }
      }
      expect(issues).toEqual([]);
    });

    it("create operations use POST", () => {
      const issues: string[] = [];
      for (const type of allTypes) {
        const def = registry.getResource(type);
        const createSpec = def.operations.create;
        if (createSpec && createSpec.method !== "POST") {
          issues.push(`${type}.create: unexpected method ${createSpec.method}`);
        }
      }
      expect(issues).toEqual([]);
    });

    it("delete operations use DELETE", () => {
      const issues: string[] = [];
      for (const type of allTypes) {
        const def = registry.getResource(type);
        const deleteSpec = def.operations.delete;
        if (deleteSpec && deleteSpec.method !== "DELETE") {
          issues.push(`${type}.delete: unexpected method ${deleteSpec.method}`);
        }
      }
      expect(issues).toEqual([]);
    });

    it("update operations use PUT or PATCH", () => {
      const issues: string[] = [];
      for (const type of allTypes) {
        const def = registry.getResource(type);
        const updateSpec = def.operations.update;
        if (updateSpec && updateSpec.method !== "PUT" && updateSpec.method !== "PATCH") {
          issues.push(`${type}.update: unexpected method ${updateSpec.method}`);
        }
      }
      expect(issues).toEqual([]);
    });
  });

  describe("responseExtractor presence", () => {
    it("every operation has a responseExtractor", () => {
      const missing: string[] = [];
      for (const type of allTypes) {
        const def = registry.getResource(type);
        for (const [op, spec] of Object.entries(def.operations)) {
          if (!spec.responseExtractor) {
            missing.push(`${type}.${op}`);
          }
        }
        for (const [action, spec] of Object.entries(def.executeActions ?? {})) {
          if (!spec.responseExtractor) {
            missing.push(`${type}.${action}`);
          }
        }
      }
      expect(missing, `Missing responseExtractor: ${missing.join(", ")}`).toEqual([]);
    });
  });

  describe("write operations require bodyBuilder or bodySchema", () => {
    it("create operations have a bodyBuilder", () => {
      const missing: string[] = [];
      for (const type of allTypes) {
        const def = registry.getResource(type);
        if (def.operations.create && !def.operations.create.bodyBuilder) {
          missing.push(`${type}.create`);
        }
      }
      expect(missing, `Missing bodyBuilder on create: ${missing.join(", ")}`).toEqual([]);
    });

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

  describe("toolset/resource cross-references", () => {
    it("every resource's toolset field matches its parent toolset name", () => {
      const issues: string[] = [];
      for (const ts of registry.getAllToolsets()) {
        for (const res of ts.resources) {
          if (res.toolset !== ts.name) {
            issues.push(`${res.resourceType}: toolset="${res.toolset}" but parent toolset is "${ts.name}"`);
          }
        }
      }
      expect(issues).toEqual([]);
    });

    it("no duplicate resource type names across toolsets", () => {
      const seen = new Map<string, string>();
      const dupes: string[] = [];
      for (const ts of registry.getAllToolsets()) {
        for (const res of ts.resources) {
          const existing = seen.get(res.resourceType);
          if (existing) {
            dupes.push(`${res.resourceType} in both "${existing}" and "${ts.name}"`);
          }
          seen.set(res.resourceType, ts.name);
        }
      }
      expect(dupes).toEqual([]);
    });
  });

  describe("operationPolicy contract", () => {
    const VALID_RISK_LEVELS = new Set(["read", "low_write", "medium_write", "high_write", "destructive"]);
    const VALID_RETRY_POLICIES = new Set(["safe", "idempotency_key_required", "do_not_retry"]);

    it("every operation endpoint spec has operationPolicy", () => {
      const missing: string[] = [];
      for (const type of allTypes) {
        const def = registry.getResource(type);
        for (const [op, spec] of Object.entries(def.operations)) {
          if (!spec.operationPolicy) {
            missing.push(`${type}.${op}`);
          }
        }
      }
      expect(missing, `Missing operationPolicy:\n${missing.join("\n")}`).toEqual([]);
    });

    it("every executeAction endpoint spec has operationPolicy", () => {
      const missing: string[] = [];
      for (const type of allTypes) {
        const def = registry.getResource(type);
        for (const [action, spec] of Object.entries(def.executeActions ?? {})) {
          if (!spec.operationPolicy) {
            missing.push(`${type}.${action}`);
          }
        }
      }
      expect(missing, `Missing operationPolicy on executeActions:\n${missing.join("\n")}`).toEqual([]);
    });

    it("operationPolicy.risk is a valid RiskLevel", () => {
      const issues: string[] = [];
      for (const type of allTypes) {
        const def = registry.getResource(type);
        for (const [op, spec] of Object.entries(def.operations)) {
          if (spec.operationPolicy && !VALID_RISK_LEVELS.has(spec.operationPolicy.risk)) {
            issues.push(`${type}.${op}: invalid risk "${spec.operationPolicy.risk}"`);
          }
        }
        for (const [action, spec] of Object.entries(def.executeActions ?? {})) {
          if (spec.operationPolicy && !VALID_RISK_LEVELS.has(spec.operationPolicy.risk)) {
            issues.push(`${type}.${action}: invalid risk "${spec.operationPolicy.risk}"`);
          }
        }
      }
      expect(issues, `Invalid risk levels:\n${issues.join("\n")}`).toEqual([]);
    });

    it("operationPolicy.retryPolicy is a valid RetryPolicy", () => {
      const issues: string[] = [];
      for (const type of allTypes) {
        const def = registry.getResource(type);
        for (const [op, spec] of Object.entries(def.operations)) {
          if (spec.operationPolicy && !VALID_RETRY_POLICIES.has(spec.operationPolicy.retryPolicy)) {
            issues.push(`${type}.${op}: invalid retryPolicy "${spec.operationPolicy.retryPolicy}"`);
          }
        }
        for (const [action, spec] of Object.entries(def.executeActions ?? {})) {
          if (spec.operationPolicy && !VALID_RETRY_POLICIES.has(spec.operationPolicy.retryPolicy)) {
            issues.push(`${type}.${action}: invalid retryPolicy "${spec.operationPolicy.retryPolicy}"`);
          }
        }
      }
      expect(issues, `Invalid retry policies:\n${issues.join("\n")}`).toEqual([]);
    });

    it("all delete operations have risk: destructive", () => {
      const issues: string[] = [];
      for (const type of allTypes) {
        const def = registry.getResource(type);
        const deleteSpec = def.operations.delete;
        if (deleteSpec?.operationPolicy && deleteSpec.operationPolicy.risk !== "destructive") {
          issues.push(`${type}.delete: risk is "${deleteSpec.operationPolicy.risk}", expected "destructive"`);
        }
      }
      expect(issues, `Delete ops without destructive risk:\n${issues.join("\n")}`).toEqual([]);
    });

    it("all list and get operations have risk: read", () => {
      const issues: string[] = [];
      for (const type of allTypes) {
        const def = registry.getResource(type);
        for (const op of ["list", "get"] as const) {
          const spec = def.operations[op];
          if (spec?.operationPolicy && spec.operationPolicy.risk !== "read") {
            issues.push(`${type}.${op}: risk is "${spec.operationPolicy.risk}", expected "read"`);
          }
        }
      }
      expect(issues, `Read ops without read risk:\n${issues.join("\n")}`).toEqual([]);
    });

    it("no endpoint spec has blockWithoutConfirmation (removed field)", () => {
      const found: string[] = [];
      for (const type of allTypes) {
        const def = registry.getResource(type);
        for (const [op, spec] of Object.entries(def.operations)) {
          if ("blockWithoutConfirmation" in spec) {
            found.push(`${type}.${op}`);
          }
        }
        for (const [action, spec] of Object.entries(def.executeActions ?? {})) {
          if ("blockWithoutConfirmation" in spec) {
            found.push(`${type}.${action}`);
          }
        }
      }
      expect(found, `Specs still have blockWithoutConfirmation:\n${found.join("\n")}`).toEqual([]);
    });
  });

  describe("write quality contract", () => {
    it("every create operation has bodySchema", () => {
      const missing: string[] = [];
      for (const type of allFullTypes) {
        const def = getFullResource(type);
        if (def.operations.create && !def.operations.create.bodySchema) {
          missing.push(`${type}.create`);
        }
      }
      expect(missing, `Missing bodySchema on create:\n${missing.join("\n")}`).toEqual([]);
    });

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

    it("every medium_write/high_write/destructive execute action has actionDescription", () => {
      const missing: string[] = [];
      const RISKY = new Set(["medium_write", "high_write", "destructive"]);
      for (const type of allFullTypes) {
        const def = getFullResource(type);
        for (const [action, spec] of Object.entries(def.executeActions ?? {})) {
          const risk = spec.operationPolicy?.risk;
          if (risk && RISKY.has(risk) && !spec.actionDescription) {
            missing.push(`${type}.${action}`);
          }
        }
      }
      expect(missing, `Missing actionDescription on medium/high-risk actions:\n${missing.join("\n")}`).toEqual([]);
    });

    it("every medium_write/high_write/destructive execute action has bodySchema", () => {
      const missing: string[] = [];
      const RISKY = new Set(["medium_write", "high_write", "destructive"]);
      for (const type of allFullTypes) {
        const def = getFullResource(type);
        for (const [action, spec] of Object.entries(def.executeActions ?? {})) {
          const risk = spec.operationPolicy?.risk;
          if (risk && RISKY.has(risk) && !spec.bodySchema) {
            missing.push(`${type}.${action}`);
          }
        }
      }
      expect(missing, `Missing bodySchema on risky execute actions:\n${missing.join("\n")}`).toEqual([]);
    });

    it("every execute action has actionDescription or description", () => {
      const missing: string[] = [];
      for (const type of allFullTypes) {
        const def = getFullResource(type);
        for (const [action, spec] of Object.entries(def.executeActions ?? {})) {
          if (!spec.actionDescription && !spec.description) {
            missing.push(`${type}.${action}`);
          }
        }
      }
      expect(missing, `Missing description on execute actions:\n${missing.join("\n")}`).toEqual([]);
    });
  });

  describe("description completeness", () => {
    it("every resource type has a non-empty description", () => {
      const empty: string[] = [];
      for (const type of allTypes) {
        const def = registry.getResource(type);
        if (!def.description || def.description.trim() === "") {
          empty.push(type);
        }
      }
      expect(empty).toEqual([]);
    });

    it("every resource type has a non-empty displayName", () => {
      const empty: string[] = [];
      for (const type of allTypes) {
        const def = registry.getResource(type);
        if (!def.displayName || def.displayName.trim() === "") {
          empty.push(type);
        }
      }
      expect(empty).toEqual([]);
    });
  });
});

describe("Opt-in toolset structural validation", () => {
  const defaultRegistry = new Registry(makeConfig());
  const defaultNames = new Set(defaultRegistry.getAllToolsets().map((t) => t.name));
  const optInNames = ALL_TOOLSET_NAMES.filter((n) => !defaultNames.has(n));

  if (optInNames.length === 0) {
    it("no opt-in toolsets to validate", () => {
      expect(optInNames).toEqual([]);
    });
    return;
  }

  const additive = optInNames.map((n) => `+${n}`).join(",");
  const expandedRegistry = new Registry({ ...makeConfig(), HARNESS_TOOLSETS: additive });
  const expandedTypes = expandedRegistry.getAllResourceTypes();

  it("expanded registry includes more resource types than defaults", () => {
    const defaultTypes = defaultRegistry.getAllResourceTypes();
    expect(expandedTypes.length).toBeGreaterThan(defaultTypes.length);
  });

  it("expanded registry includes all opt-in toolsets", () => {
    const loadedNames = new Set(expandedRegistry.getAllToolsets().map((t) => t.name));
    const missing = optInNames.filter((n) => !loadedNames.has(n));
    expect(missing, `Opt-in toolsets not loaded: ${missing.join(", ")}`).toEqual([]);
  });

  it("every opt-in resource type has identifierFields defined", () => {
    const missing: string[] = [];
    for (const type of expandedTypes) {
      const def = expandedRegistry.getResource(type);
      if (!def.identifierFields) {
        missing.push(type);
      }
    }
    expect(missing, `Missing identifierFields: ${missing.join(", ")}`).toEqual([]);
  });

  it("every opt-in operation has a responseExtractor", () => {
    const missing: string[] = [];
    for (const type of expandedTypes) {
      const def = expandedRegistry.getResource(type);
      for (const [op, spec] of Object.entries(def.operations)) {
        if (!spec.responseExtractor) missing.push(`${type}.${op}`);
      }
      for (const [action, spec] of Object.entries(def.executeActions ?? {})) {
        if (!spec.responseExtractor) missing.push(`${type}.${action}`);
      }
    }
    expect(missing, `Missing responseExtractor: ${missing.join(", ")}`).toEqual([]);
  });

  it("every opt-in operation has operationPolicy", () => {
    const missing: string[] = [];
    for (const type of expandedTypes) {
      const def = expandedRegistry.getResource(type);
      for (const [op, spec] of Object.entries(def.operations)) {
        if (!spec.operationPolicy) missing.push(`${type}.${op}`);
      }
      for (const [action, spec] of Object.entries(def.executeActions ?? {})) {
        if (!spec.operationPolicy) missing.push(`${type}.${action}`);
      }
    }
    expect(missing, `Missing operationPolicy:\n${missing.join("\n")}`).toEqual([]);
  });

  it("every opt-in resource has a valid scope", () => {
    const validScopes = new Set(["project", "org", "account"]);
    const invalid: string[] = [];
    for (const type of expandedTypes) {
      const def = expandedRegistry.getResource(type);
      if (!validScopes.has(def.scope)) {
        invalid.push(`${type}: scope="${def.scope}"`);
      }
    }
    expect(invalid).toEqual([]);
  });

  it("every opt-in resource has a non-empty description and displayName", () => {
    const empty: string[] = [];
    for (const type of expandedTypes) {
      const def = expandedRegistry.getResource(type);
      if (!def.description?.trim()) empty.push(`${type}: missing description`);
      if (!def.displayName?.trim()) empty.push(`${type}: missing displayName`);
    }
    expect(empty).toEqual([]);
  });
});
