import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { validateDuration, validateServiceName, sanitizeLabelValue, validateLogLabels, validateLogAggregate, validateMetricLabels, validateRawQuery, errorResponse } from "./validation.js";

describe("validateLogAggregate (Q-LOG2)", () => {
  it("accepts undefined and valid specs", () => {
    assert.equal(validateLogAggregate(undefined), null);
    assert.equal(validateLogAggregate({ op: "count_over_time" }), null);
    assert.equal(validateLogAggregate({ op: "sum", by: ["url", "status"] }), null);
    assert.equal(validateLogAggregate({ op: "topk", by: ["url"], k: 5, step: "15m" }), null);
  });
  it("rejects a bad/missing op", () => {
    assert.ok(validateLogAggregate({}));
    assert.ok(validateLogAggregate({ op: "median" }));
    assert.ok(validateLogAggregate("nope"));
  });
  it("rejects bad by labels", () => {
    assert.ok(validateLogAggregate({ op: "sum", by: ["a.b"] }));
    assert.ok(validateLogAggregate({ op: "sum", by: "url" as unknown as string[] }));
  });
  it("rejects bad k and step", () => {
    assert.ok(validateLogAggregate({ op: "topk", by: ["url"], k: 0 }));
    assert.ok(validateLogAggregate({ op: "topk", by: ["url"], k: 99999 }));
    assert.ok(validateLogAggregate({ op: "count_over_time", step: "soon" }));
  });
  it("requires a by label for topk", () => {
    assert.ok(validateLogAggregate({ op: "topk" }));
    assert.ok(validateLogAggregate({ op: "topk", by: [] }));
  });
});

describe("validateLogLabels (Q-LOG1)", () => {
  it("accepts undefined (no filter) and a valid map", () => {
    assert.equal(validateLogLabels(undefined), null);
    assert.equal(validateLogLabels({ method: "GET", status: "200", environment: "prod" }), null);
  });
  it("rejects non-object inputs", () => {
    assert.ok(validateLogLabels("nope"));
    assert.ok(validateLogLabels(["a"]));
    assert.ok(validateLogLabels(42));
  });
  it("rejects label names with dots, dashes, or quotes (injection-safe, fail-closed)", () => {
    assert.ok(validateLogLabels({ "a.b": "x" }));
    assert.ok(validateLogLabels({ "a-b": "x" }));
    assert.ok(validateLogLabels({ 'a"x': "y" }));
    assert.ok(validateLogLabels({ "1abc": "x" })); // can't start with a digit
  });
  it("rejects non-string and over-long values", () => {
    assert.ok(validateLogLabels({ method: 123 as unknown as string }));
    assert.ok(validateLogLabels({ method: "x".repeat(1025) }));
  });
  it("rejects too many labels", () => {
    const many: Record<string, string> = {};
    for (let i = 0; i < 21; i++) many[`k${i}`] = "v";
    assert.ok(validateLogLabels(many));
  });
});

describe("validateRawQuery (R4, issue #415 #3)", () => {
  it("accepts undefined (not requested) and a normal query", () => {
    assert.equal(validateRawQuery(undefined), null);
    assert.equal(validateRawQuery('sum(rate(http_requests_total[5m]))'), null);
  });
  it("rejects non-strings, empty/whitespace, and over-long", () => {
    assert.ok(validateRawQuery(42));
    assert.ok(validateRawQuery(""));
    assert.ok(validateRawQuery("   "));
    assert.ok(validateRawQuery("x".repeat(8193)));
  });
});

describe("validateMetricLabels (R3, issue #415 #4)", () => {
  it("accepts undefined and a valid map (same rules as log labels)", () => {
    assert.equal(validateMetricLabels(undefined), null);
    assert.equal(validateMetricLabels({ status: "500", route: "/checkout" }), null);
  });
  it("rejects invalid label names and bad values, fail-closed", () => {
    assert.ok(validateMetricLabels({ "a-b": "x" }));
    assert.ok(validateMetricLabels({ status: 500 as unknown as string }));
    assert.ok(validateMetricLabels("nope"));
  });
});

describe("validateDuration", () => {
  it("accepts valid durations", () => {
    assert.equal(validateDuration("5m"), null);
    assert.equal(validateDuration("1h"), null);
    assert.equal(validateDuration("24h"), null);
    assert.equal(validateDuration("7d"), null);
    assert.equal(validateDuration("30m"), null);
    assert.equal(validateDuration("365d"), null);
  });

  it("rejects invalid durations", () => {
    assert.ok(validateDuration("") !== null);
    assert.ok(validateDuration("5") !== null);
    assert.ok(validateDuration("m") !== null);
    assert.ok(validateDuration("5s") !== null);       // seconds not supported
    assert.ok(validateDuration("5min") !== null);      // only single-char units
    assert.ok(validateDuration("-5m") !== null);        // no negative
    assert.ok(validateDuration("5.5m") !== null);       // no decimals
    assert.ok(validateDuration("5m 1h") !== null);      // no spaces
    assert.ok(validateDuration("abc") !== null);
  });

  it("returns helpful error message", () => {
    const err = validateDuration("5s");
    assert.ok(err!.includes("Invalid duration"));
    assert.ok(err!.includes("5m"));  // example in message
  });
});

describe("sanitizeLabelValue", () => {
  it("accepts valid label values", () => {
    assert.equal(sanitizeLabelValue("api-gateway"), "api-gateway");
    assert.equal(sanitizeLabelValue("payment_service"), "payment_service");
    assert.equal(sanitizeLabelValue("svc.prod.us-east-1"), "svc.prod.us-east-1");
    assert.equal(sanitizeLabelValue("host:8080"), "host:8080");
    assert.equal(sanitizeLabelValue("a"), "a");
  });

  it("rejects empty string", () => {
    assert.equal(sanitizeLabelValue(""), null);
  });

  it("rejects strings over 128 characters", () => {
    assert.equal(sanitizeLabelValue("a".repeat(129)), null);
  });

  it("accepts exactly 128 characters", () => {
    assert.equal(sanitizeLabelValue("a".repeat(128)), "a".repeat(128));
  });

  it("rejects injection attempts", () => {
    assert.equal(sanitizeLabelValue('service"}[5m])'), null);  // PromQL injection
    assert.equal(sanitizeLabelValue("service`rm -rf`"), null); // command injection
    assert.equal(sanitizeLabelValue("svc;drop"), null);        // semicolon
    assert.equal(sanitizeLabelValue("svc name"), null);        // spaces
    assert.equal(sanitizeLabelValue('svc"name'), null);        // quotes
    assert.equal(sanitizeLabelValue("svc'name"), null);        // single quotes
    assert.equal(sanitizeLabelValue("svc{name}"), null);       // braces
    assert.equal(sanitizeLabelValue("svc|name"), null);        // pipe
  });
});

describe("validateServiceName", () => {
  it("returns null for valid service names", () => {
    assert.equal(validateServiceName("api-gateway"), null);
    assert.equal(validateServiceName("payment-service"), null);
    assert.equal(validateServiceName("my_svc_v2"), null);
  });

  it("returns error for invalid service names", () => {
    assert.ok(validateServiceName("") !== null);
    assert.ok(validateServiceName("svc name") !== null);
    assert.ok(validateServiceName('svc"injection') !== null);
  });

  it("returns helpful error message", () => {
    const err = validateServiceName("bad name!");
    assert.ok(err!.includes("Invalid service name"));
    assert.ok(err!.includes("alphanumeric"));
  });
});

describe("errorResponse", () => {
  it("returns MCP error format", () => {
    const res = errorResponse("something went wrong");
    assert.equal(res.isError, true);
    assert.equal(res.content.length, 1);
    assert.equal(res.content[0].type, "text");
    const parsed = JSON.parse(res.content[0].text);
    assert.equal(parsed.error, "something went wrong");
  });
});
