import { afterEach, expect, test } from "bun:test";
import { mkdtempSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { spawnSync } from "node:child_process";
import {
  featureConfigOtelPayload,
  runFeatureConfigSchemaValidation,
  validateFeatureConfigSchema,
} from "./feature-config-schema-warning.mjs";

const roots: string[] = [];
const validatorScript = resolve(import.meta.dir, "feature-config-schema-warning.mjs");
const vendorBundle = resolve(import.meta.dir, "../../vendor/ajv-dist/8.17.1/ajv2020.min.js");

afterEach(() => {
  while (roots.length > 0) rmSync(roots.pop()!, { recursive: true, force: true });
});

test("valid nested Draft 2020-12 candidate remains warning-free", () => {
  const root = fixture(schema({
    tracePanel: feature("trace-panel", {
      type: "object",
      required: ["enabled", "views"],
      properties: {
        enabled: { type: "boolean" },
        views: { type: "array", minItems: 1, items: { type: "string", pattern: "^[a-z-]+$" } },
      },
      additionalProperties: false,
    }),
  }), { tracePanel: '{"enabled":true,"views":["timeline"]}' });
  expect(validateFeatureConfigSchema({ repoDir: root, renderedRoot: join(root, "rendered") })).toMatchObject({
    warning: false,
    blocking: false,
    code: "feature-config-schema-valid",
    errorCount: 0,
    mutation: false,
    candidate: { source: "rendered-workload-env", matchedValueCount: 1 },
  });
});

test("repo-owned Ajv 2020 bundle performs real validation without workspace dependencies", () => {
  const root = fixture(schema({ tracePanel: feature("trace-panel", { type: "boolean" }) }), { tracePanel: "true" });
  const run = runValidator(root, vendorBundle);
  expect(run.status).toBe(0);
  expect(run.stderr).toContain('"code":"feature-config-schema-valid"');
  expect(run.stderr).not.toContain("feature-config-validator-unavailable");
});

test("missing or corrupt Ajv bundle is a typed warning and exits zero", () => {
  const root = fixture(schema({ tracePanel: feature("trace-panel", { type: "boolean" }) }), { tracePanel: "true" });
  const missing = runValidator(root, join(root, "missing-ajv2020.min.js"));
  expect(missing.status).toBe(0);
  expect(missing.stderr).toContain('"code":"feature-config-validator-unavailable"');

  const corruptBundle = join(root, "corrupt-ajv2020.min.js");
  writeFileSync(corruptBundle, "this is not JavaScript");
  const corrupt = runValidator(root, corruptBundle);
  expect(corrupt.status).toBe(0);
  expect(corrupt.stderr).toContain('"code":"feature-config-validator-unavailable"');
});

test("missing schema is a typed non-blocking warning", () => {
  const root = fixture();
  expect(validateFeatureConfigSchema({ repoDir: root, renderedRoot: join(root, "rendered") })).toMatchObject({
    warning: true,
    blocking: false,
    code: "feature-config-schema-missing",
  });
});

test("invalid JSON and unsupported Draft 2020-12 schema are typed warnings", () => {
  const invalidJsonRoot = fixture("{");
  expect(validateFeatureConfigSchema({ repoDir: invalidJsonRoot, renderedRoot: join(invalidJsonRoot, "rendered") }).code).toBe("feature-config-schema-invalid");

  const invalidSchemaRoot = fixture(JSON.stringify({ $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", properties: { tracePanel: { type: "not-a-json-schema-type", "x-unidesk-feature": "trace-panel" } } }));
  expect(validateFeatureConfigSchema({ repoDir: invalidSchemaRoot, renderedRoot: join(invalidSchemaRoot, "rendered") }).code).toBe("feature-config-schema-invalid");
});

test("missing rendered candidate is distinct from schema failure", () => {
  const root = fixture(schema({ tracePanel: feature("trace-panel", { type: "boolean" }) }));
  expect(validateFeatureConfigSchema({ repoDir: root, renderedRoot: join(root, "missing") })).toMatchObject({
    warning: true,
    blocking: false,
    code: "feature-config-input-missing",
  });
});

test("nested value mismatch is bounded and does not disclose the value", () => {
  const root = fixture(schema({ tracePanel: feature("trace-panel", { type: "object", required: ["enabled"], properties: { enabled: { const: true } } }) }), {
    tracePanel: '{"enabled":false,"secret":"do-not-print"}',
  });
  const result = validateFeatureConfigSchema({ repoDir: root, renderedRoot: join(root, "rendered") });
  expect(result).toMatchObject({ warning: true, blocking: false, code: "feature-config-value-mismatch", valuesPrinted: false });
  expect(result.firstError).not.toContain("do-not-print");
});

test("duplicate feature properties are reported without blocking", () => {
  const root = fixture(schema({
    tracePanel: feature("trace-panel", { type: "boolean" }),
    tracePanelAlias: feature("trace-panel", { type: "boolean" }),
  }));
  expect(validateFeatureConfigSchema({ repoDir: root, renderedRoot: join(root, "rendered") })).toMatchObject({
    warning: true,
    blocking: false,
    code: "feature-config-variable-duplicate",
    errorCount: 1,
  });
});

test("actual OTLP span and event carry redacted validation attributes", () => {
  const root = fixture();
  const validation = validateFeatureConfigSchema({ repoDir: root, renderedRoot: join(root, "rendered") });
  const payload = featureConfigOtelPayload(validation, {
    serviceName: "unidesk-cicd",
    consumer: "hwlab-nc01-v03",
    samplingRatio: 1,
    nowMs: 1,
    traceId: "1".repeat(32),
    spanId: "2".repeat(16),
  });
  const span = payload.resourceSpans[0].scopeSpans[0].spans[0];
  expect(span.name).toBe("cicd.feature_config.schema");
  expect(span.events[0].name).toBe("feature-config-schema-validation");
  expect(JSON.stringify(payload)).not.toContain("do-not-print");
  expect(JSON.stringify(payload)).toContain("unidesk.otel.sampling_ratio");
});

test("missing endpoint remains a non-blocking OTel warning", async () => {
  const root = fixture();
  const result = await runFeatureConfigSchemaValidation({
    repoDir: root,
    renderedRoot: join(root, "rendered"),
    otelEndpoint: "",
    otelTimeoutMs: 300,
  });
  expect(result.otelWarning).toMatchObject({
    event: "cicd.otel.export",
    warning: true,
    blocking: false,
    causeCode: "ENDPOINT_MISSING",
    timeoutMs: 300,
  });
});

test("export failure uses configured timeout and remains non-blocking", async () => {
  const root = fixture();
  let requestSignal: AbortSignal | null = null;
  const result = await runFeatureConfigSchemaValidation({
    repoDir: root,
    renderedRoot: join(root, "rendered"),
    otelEndpoint: "http://otel.example.test/v1/traces",
    otelServiceName: "unidesk-cicd",
    otelSamplingRatio: 1,
    otelTimeoutMs: 300,
    fetchImpl: async (_input: URL | RequestInfo, init?: RequestInit) => {
      requestSignal = init?.signal ?? null;
      throw new Error("collector unavailable");
    },
  });
  expect(requestSignal).not.toBeNull();
  expect(result.otelWarning).toMatchObject({
    event: "cicd.otel.export",
    warning: true,
    blocking: false,
    causeCode: "EXPORT_FAILED",
    timeoutMs: 300,
  });
});

test("unexpected validator failure is typed and never rejects", async () => {
  const options: Record<string, unknown> = {
    repoDir: fixture(schema({ webProbeSentinel: feature("web-probe-sentinel", { type: "object" }) })),
    renderedRoot: "unused",
    otelEndpoint: "",
    otelTimeoutMs: 300,
  };
  Object.defineProperty(options, "candidate", { get: () => { throw new Error("unexpected candidate failure"); } });
  const result = await runFeatureConfigSchemaValidation(options);
  expect(result.validation).toMatchObject({
    warning: true,
    blocking: false,
    code: "feature-config-validator-failure",
    mutation: false,
  });
  expect(result.otelWarning).toMatchObject({ warning: true, blocking: false, causeCode: "ENDPOINT_MISSING" });
});

test("candidate scan ignores directory symlinks and bounds directory traversal", () => {
  const root = fixture(schema({ webProbeSentinel: feature("web-probe-sentinel", { type: "boolean" }) }), {
    webProbeSentinel: "true",
  });
  symlinkSync(join(root, "rendered"), join(root, "rendered", "loop"), "dir");
  expect(validateFeatureConfigSchema({ repoDir: root, renderedRoot: join(root, "rendered") })).toMatchObject({
    warning: false,
    code: "feature-config-schema-valid",
  });
  for (let index = 0; index <= 2_000; index += 1) mkdirSync(join(root, "rendered", `directory-${index}`));
  expect(validateFeatureConfigSchema({ repoDir: root, renderedRoot: join(root, "rendered") })).toMatchObject({
    warning: true,
    blocking: false,
    code: "feature-config-validator-failure",
    firstError: "rendered workload directory limit exceeded: 2000",
  });
});

function fixture(content?: string, env: Record<string, string> = {}) {
  const root = mkdtempSync(join(tmpdir(), "feature-config-schema-"));
  roots.push(root);
  mkdirSync(join(root, "rendered"));
  if (content !== undefined) {
    mkdirSync(join(root, "config"));
    writeFileSync(join(root, "config", "feature-config.schema.json"), content);
  }
  writeFileSync(join(root, "rendered", "deployment.yaml"), deployment(env));
  return root;
}

function runValidator(root: string, bundlePath: string) {
  return spawnSync(process.execPath, [validatorScript, "--rendered-root", join(root, "rendered")], {
    cwd: root,
    encoding: "utf8",
    env: {
      ...process.env,
      UNIDESK_AJV2020_BUNDLE: bundlePath,
      OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "",
      OTEL_EXPORTER_TIMEOUT_MS: "300",
    },
  });
}

function schema(properties: Record<string, unknown>) {
  return JSON.stringify({
    $schema: "https://json-schema.org/draft/2020-12/schema",
    type: "object",
    properties,
    additionalProperties: false,
  });
}

function feature(name: string, value: Record<string, unknown>) {
  return { ...value, "x-unidesk-feature": name };
}

function deployment(env: Record<string, string>) {
  const rows = Object.entries(env).map(([name, value]) => `            - name: ${name}\n              value: ${JSON.stringify(value)}`).join("\n");
  return `apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: fixture\nspec:\n  template:\n    spec:\n      containers:\n        - name: fixture\n          env:\n${rows || "            []"}\n`;
}
