import { describe, it, expect, beforeEach } from "vitest";
import {
  BasicTracerProvider,
  InMemorySpanExporter,
  SimpleSpanProcessor,
} from "@opentelemetry/sdk-trace-base";
import {
  LoggerProvider,
  InMemoryLogRecordExporter,
  SimpleLogRecordProcessor,
} from "@opentelemetry/sdk-logs";
import { SeverityNumber } from "@opentelemetry/api-logs";
import {
  PiiScrubSpanProcessor,
  PiiScrubLogRecordProcessor,
} from "./pii-scrub-processor";

const spanExporter = new InMemorySpanExporter();
const tracerProvider = new BasicTracerProvider({
  spanProcessors: [
    new PiiScrubSpanProcessor(),
    new SimpleSpanProcessor(spanExporter),
  ],
});

// Use addLogRecordProcessor to chain processors in the right order.
const logExporter = new InMemoryLogRecordExporter();
const logProvider = new LoggerProvider();
logProvider.addLogRecordProcessor(new PiiScrubLogRecordProcessor());
logProvider.addLogRecordProcessor(new SimpleLogRecordProcessor(logExporter));

beforeEach(() => {
  spanExporter.reset();
  logExporter.reset();
});

describe("PiiScrubSpanProcessor", () => {
  it("redacts attributes whose names contain PII substrings", () => {
    const tracer = tracerProvider.getTracer("test");
    const span = tracer.startSpan("test-span", {
      attributes: {
        "user.email": "alice@example.com",
        "user.id": "u_123",
        "auth.token": "secret-token",
        "request.path": "/api/users",
      },
    });
    span.end();
    const exported = spanExporter.getFinishedSpans();
    expect(exported[0]!.attributes["user.email"]).toBe("[redacted]");
    expect(exported[0]!.attributes["auth.token"]).toBe("[redacted]");
    expect(exported[0]!.attributes["user.id"]).toBe("u_123"); // id is fine
    expect(exported[0]!.attributes["request.path"]).toBe("/api/users");
  });

  it("preserves non-PII attributes unchanged", () => {
    const tracer = tracerProvider.getTracer("test");
    const span = tracer.startSpan("test-span", {
      attributes: {
        "http.method": "GET",
        "span.op": "use-case",
        feature: "blog",
      },
    });
    span.end();
    const exported = spanExporter.getFinishedSpans();
    expect(exported[0]!.attributes["http.method"]).toBe("GET");
    expect(exported[0]!.attributes["span.op"]).toBe("use-case");
    expect(exported[0]!.attributes["feature"]).toBe("blog");
  });

  it("redacts attributes with cookie and apikey substrings", () => {
    const tracer = tracerProvider.getTracer("test");
    const span = tracer.startSpan("test-span", {
      attributes: {
        "request.cookie": "session=xyz",
        "x-api-key": "key123",
        "secret.value": "mysecret",
      },
    });
    span.end();
    const exported = spanExporter.getFinishedSpans();
    expect(exported[0]!.attributes["request.cookie"]).toBe("[redacted]");
    expect(exported[0]!.attributes["x-api-key"]).toBe("[redacted]");
    expect(exported[0]!.attributes["secret.value"]).toBe("[redacted]");
  });
});

describe("PiiScrubLogRecordProcessor", () => {
  it("redacts log record attributes whose names contain PII substrings", () => {
    const logger = logProvider.getLogger("test");
    logger.emit({
      severityNumber: SeverityNumber.ERROR,
      severityText: "ERROR",
      body: "test",
      attributes: {
        "user.email": "alice@example.com",
        "exception.message": "boom",
      },
    });
    const records = logExporter.getFinishedLogRecords();
    expect(records[0]!.attributes!["user.email"]).toBe("[redacted]");
    expect(records[0]!.attributes!["exception.message"]).toBe("boom");
  });

  it("redacts log body when it contains PII substrings", () => {
    const logger = logProvider.getLogger("test");
    logger.emit({
      severityNumber: SeverityNumber.INFO,
      severityText: "INFO",
      body: "user signed in with email alice@example.com",
    });
    const records = logExporter.getFinishedLogRecords();
    expect(records[0]!.body).toBe("[redacted]");
  });

  it("preserves log body when it contains no PII substrings", () => {
    const logger = logProvider.getLogger("test");
    logger.emit({
      severityNumber: SeverityNumber.INFO,
      severityText: "INFO",
      body: "user signed in successfully",
    });
    const records = logExporter.getFinishedLogRecords();
    expect(records[0]!.body).toBe("user signed in successfully");
  });

  it("scrubs IPv4 in log record body", () => {
    const logger = logProvider.getLogger("test");
    logger.emit({
      severityNumber: SeverityNumber.INFO,
      severityText: "INFO",
      body: "request from 10.0.0.1 finished",
    });
    const records = logExporter.getFinishedLogRecords();
    expect(records[0]!.body).toContain("[redacted-ip]");
    expect(records[0]!.body).not.toContain("10.0.0.1");
  });
});

describe("PiiScrubSpanProcessor — IP address scrubbing", () => {
  it("scrubs IPv4 addresses in attribute values", () => {
    const tracer = tracerProvider.getTracer("test");
    const span = tracer.startSpan("test-span", {
      attributes: { "request.note": "request from 10.0.0.1" },
    });
    span.end();
    const exported = spanExporter.getFinishedSpans();
    expect(exported[0]!.attributes["request.note"]).toBe(
      "request from [redacted-ip]",
    );
  });

  it("scrubs IPv6 addresses in attribute values", () => {
    const tracer = tracerProvider.getTracer("test");
    const span = tracer.startSpan("test-span", {
      attributes: { "request.note": "request from 2001:0db8::1" },
    });
    span.end();
    const exported = spanExporter.getFinishedSpans();
    expect(exported[0]!.attributes["request.note"]).toContain("[redacted-ip]");
    expect(exported[0]!.attributes["request.note"]).not.toContain("2001:0db8");
  });

  it("redacts http.client_ip via key match (semconv 1.20)", () => {
    const tracer = tracerProvider.getTracer("test");
    const span = tracer.startSpan("test-span", {
      attributes: { "http.client_ip": "10.0.0.1" },
    });
    span.end();
    const exported = spanExporter.getFinishedSpans();
    expect(exported[0]!.attributes["http.client_ip"]).toBe("[redacted]");
  });

  it("redacts client.address via key match (semconv 1.27+)", () => {
    const tracer = tracerProvider.getTracer("test");
    const span = tracer.startSpan("test-span", {
      attributes: { "client.address": "10.0.0.1" },
    });
    span.end();
    const exported = spanExporter.getFinishedSpans();
    expect(exported[0]!.attributes["client.address"]).toBe("[redacted]");
  });
});
