/**
 * Tests for lib/request-logger.ts — Canonical Log Lines
 *
 * Covers:
 *  - categorizeRequest: all category paths + edge cases
 *  - buildCanonicalLogLine: field population, duration calc, IP sanitization, UA truncation
 *  - formatCanonicalLog: level mapping, structured JSON output, env vars
 */

import {
  categorizeRequest,
  buildCanonicalLogLine,
  formatCanonicalLog,
  type CanonicalLogLine,
} from "@/lib/request-logger";

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/** Default params for buildCanonicalLogLine — override per test */
function defaultBuildParams(overrides: Record<string, unknown> = {}) {
  return {
    method: "GET",
    pathname: "/api/health/ready",
    statusCode: 200,
    startTime: Date.now() - 42,
    correlationId: "corr-abc-123",
    requestId: "req-xyz-789",
    clientIp: "192.168.1.100",
    userAgent: "Mozilla/5.0 TestAgent",
    contentType: "application/json",
    rateLimited: false,
    responseSize: 512,
    ...overrides,
  };
}

/** Build a minimal CanonicalLogLine for formatCanonicalLog tests */
function defaultLogLine(
  overrides: Partial<CanonicalLogLine> = {},
): CanonicalLogLine {
  return {
    timestamp: "2026-02-23T12:00:00.000Z",
    method: "GET",
    path: "/api/health/ready",
    statusCode: 200,
    durationMs: 15,
    correlationId: "corr-abc-123",
    requestId: "req-xyz-789",
    clientIp: "192.168.1.x.x",
    userAgent: "TestAgent/1.0",
    contentType: "application/json",
    rateLimited: false,
    category: "api:health",
    ...overrides,
  };
}

// ---------------------------------------------------------------------------
// categorizeRequest
// ---------------------------------------------------------------------------

describe("categorizeRequest", () => {
  describe("API routes", () => {
    it("maps /api/auth/* to api:auth", () => {
      expect(categorizeRequest("/api/auth/login")).toBe("api:auth");
      expect(categorizeRequest("/api/auth/verify-login-otp")).toBe("api:auth");
      expect(categorizeRequest("/api/auth/csrf")).toBe("api:auth");
    });

    it("maps /api/staff/* to api:staff", () => {
      expect(categorizeRequest("/api/staff/dashboard/overview")).toBe(
        "api:staff",
      );
      expect(categorizeRequest("/api/staff/users/123/billing/invoices")).toBe(
        "api:staff",
      );
    });

    it("maps /api/billing/* to api:billing", () => {
      expect(categorizeRequest("/api/billing/current")).toBe("api:billing");
      expect(categorizeRequest("/api/billing/invoices")).toBe("api:billing");
    });

    it("maps /api/stripe/* to api:billing", () => {
      expect(categorizeRequest("/api/stripe/webhook")).toBe("api:billing");
      expect(categorizeRequest("/api/stripe/billing-portal")).toBe(
        "api:billing",
      );
    });

    it("maps /api/subscription/* to api:billing", () => {
      expect(categorizeRequest("/api/subscription/status")).toBe("api:billing");
      expect(categorizeRequest("/api/subscription/upgrade")).toBe(
        "api:billing",
      );
    });

    it("maps /api/subscriptions/* to api:billing", () => {
      expect(categorizeRequest("/api/subscriptions/create-checkout")).toBe(
        "api:billing",
      );
    });

    it("maps /api/dashboard/* to api:dashboard", () => {
      expect(categorizeRequest("/api/dashboard/workflows")).toBe(
        "api:dashboard",
      );
      expect(categorizeRequest("/api/dashboard/knowledge-base/documents")).toBe(
        "api:dashboard",
      );
    });

    it("maps /api/health/* to api:health", () => {
      expect(categorizeRequest("/api/health/ready")).toBe("api:health");
      expect(categorizeRequest("/api/health/live")).toBe("api:health");
      expect(categorizeRequest("/api/health/database")).toBe("api:health");
    });

    it("maps other /api/* routes to api:public", () => {
      expect(categorizeRequest("/api/contact/submit")).toBe("api:public");
      expect(categorizeRequest("/api/demo/book")).toBe("api:public");
      expect(categorizeRequest("/api/register")).toBe("api:public");
      expect(categorizeRequest("/api/send-otp")).toBe("api:public");
    });
  });

  describe("Page routes", () => {
    it("maps paths containing /dashboard to page:dashboard", () => {
      expect(categorizeRequest("/en/dashboard")).toBe("page:dashboard");
      expect(categorizeRequest("/ar/dashboard/billing")).toBe("page:dashboard");
      expect(categorizeRequest("/en/dashboard/workflows")).toBe(
        "page:dashboard",
      );
    });

    it("maps /staff/* to page:staff", () => {
      expect(categorizeRequest("/staff/login")).toBe("page:staff");
      expect(categorizeRequest("/staff/settings")).toBe("page:staff");
    });

    it('maps /staff/dashboard to page:dashboard because .includes("/dashboard") is checked first', () => {
      // The .includes('/dashboard') check precedes .startsWith('/staff/') in categorizeRequest,
      // so /staff/dashboard is categorized as page:dashboard, not page:staff.
      expect(categorizeRequest("/staff/dashboard")).toBe("page:dashboard");
    });

    it("maps /_next/* to static", () => {
      expect(categorizeRequest("/_next/static/chunks/main.js")).toBe("static");
      expect(categorizeRequest("/_next/data/abc123/en.json")).toBe("static");
    });

    it("maps everything else to page:public", () => {
      expect(categorizeRequest("/en/pricing")).toBe("page:public");
      expect(categorizeRequest("/ar/about")).toBe("page:public");
      expect(categorizeRequest("/en/blog/my-post")).toBe("page:public");
    });
  });

  describe("Edge cases", () => {
    it("maps root path / to page:public", () => {
      expect(categorizeRequest("/")).toBe("page:public");
    });

    it("maps unknown nested paths to page:public", () => {
      expect(categorizeRequest("/some/random/path")).toBe("page:public");
    });

    it("prefers more specific api:* category over page:dashboard for /api/dashboard/", () => {
      // /api/dashboard/ should hit api:dashboard before the .includes('/dashboard') check
      expect(categorizeRequest("/api/dashboard/voice-agents")).toBe(
        "api:dashboard",
      );
    });

    it('maps /staff/dashboard to page:dashboard because .includes("/dashboard") precedes .startsWith("/staff/")', () => {
      expect(categorizeRequest("/staff/dashboard")).toBe("page:dashboard");
    });
  });
});

// ---------------------------------------------------------------------------
// buildCanonicalLogLine
// ---------------------------------------------------------------------------

describe("buildCanonicalLogLine", () => {
  const originalEnv = process.env.NODE_ENV;

  afterEach(() => {
    process.env.NODE_ENV = originalEnv;
    jest.restoreAllMocks();
  });

  it("populates all required fields correctly", () => {
    const result = buildCanonicalLogLine(defaultBuildParams());

    expect(result).toMatchObject({
      method: "GET",
      path: "/api/health/ready",
      statusCode: 200,
      correlationId: "corr-abc-123",
      requestId: "req-xyz-789",
      contentType: "application/json",
      rateLimited: false,
      responseSize: 512,
      category: "api:health",
    });
    expect(result.timestamp).toBeDefined();
    expect(typeof result.durationMs).toBe("number");
  });

  describe("durationMs calculation", () => {
    it("calculates durationMs as Date.now() minus startTime", () => {
      const mockNow = 1000100;
      jest.spyOn(Date, "now").mockReturnValue(mockNow);

      const result = buildCanonicalLogLine(
        defaultBuildParams({ startTime: 1000000 }),
      );

      expect(result.durationMs).toBe(100);
    });

    it("returns 0 when startTime equals current time", () => {
      const mockNow = 5000;
      jest.spyOn(Date, "now").mockReturnValue(mockNow);

      const result = buildCanonicalLogLine(
        defaultBuildParams({ startTime: 5000 }),
      );

      expect(result.durationMs).toBe(0);
    });
  });

  describe("IP sanitization", () => {
    it("sanitizes IPv4 in production — keeps first 2 octets, masks rest", () => {
      process.env.NODE_ENV = "production";

      const result = buildCanonicalLogLine(
        defaultBuildParams({ clientIp: "10.20.30.40" }),
      );

      expect(result.clientIp).toBe("10.20.x.x");
    });

    it("masks IPv6 completely in production", () => {
      process.env.NODE_ENV = "production";

      const result = buildCanonicalLogLine(
        defaultBuildParams({
          clientIp: "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
        }),
      );

      expect(result.clientIp).toBe("[masked]");
    });

    it("masks ::1 (IPv6 loopback) in production", () => {
      process.env.NODE_ENV = "production";

      const result = buildCanonicalLogLine(
        defaultBuildParams({ clientIp: "::1" }),
      );

      expect(result.clientIp).toBe("[masked]");
    });

    it("does NOT sanitize IPv4 in development", () => {
      process.env.NODE_ENV = "development";

      const result = buildCanonicalLogLine(
        defaultBuildParams({ clientIp: "10.20.30.40" }),
      );

      expect(result.clientIp).toBe("10.20.30.40");
    });

    it("does NOT sanitize IPv6 in development", () => {
      process.env.NODE_ENV = "development";

      const result = buildCanonicalLogLine(
        defaultBuildParams({ clientIp: "::1" }),
      );

      expect(result.clientIp).toBe("::1");
    });

    it("does NOT sanitize in test environment", () => {
      process.env.NODE_ENV = "test";

      const result = buildCanonicalLogLine(
        defaultBuildParams({ clientIp: "172.16.0.1" }),
      );

      expect(result.clientIp).toBe("172.16.0.1");
    });
  });

  describe("User-Agent handling", () => {
    it("truncates user-agent at 120 characters", () => {
      const longUA = "A".repeat(200);

      const result = buildCanonicalLogLine(
        defaultBuildParams({ userAgent: longUA }),
      );

      expect(result.userAgent).toBe("A".repeat(120) + "...");
      expect(result.userAgent.length).toBe(123); // 120 + '...'
    });

    it("does not truncate user-agent at exactly 120 characters", () => {
      const exactUA = "B".repeat(120);

      const result = buildCanonicalLogLine(
        defaultBuildParams({ userAgent: exactUA }),
      );

      expect(result.userAgent).toBe(exactUA);
      expect(result.userAgent.length).toBe(120);
    });

    it("passes through user-agent shorter than 120 characters unchanged", () => {
      const shortUA = "Mozilla/5.0 (Macintosh)";

      const result = buildCanonicalLogLine(
        defaultBuildParams({ userAgent: shortUA }),
      );

      expect(result.userAgent).toBe(shortUA);
    });

    it('returns "unknown" for empty user-agent', () => {
      const result = buildCanonicalLogLine(
        defaultBuildParams({ userAgent: "" }),
      );

      expect(result.userAgent).toBe("unknown");
    });
  });

  describe("Content-Type handling", () => {
    it('sets contentType to "none" when empty string', () => {
      const result = buildCanonicalLogLine(
        defaultBuildParams({ contentType: "" }),
      );

      expect(result.contentType).toBe("none");
    });

    it("preserves contentType when provided", () => {
      const result = buildCanonicalLogLine(
        defaultBuildParams({ contentType: "text/html" }),
      );

      expect(result.contentType).toBe("text/html");
    });
  });

  describe("Category assignment", () => {
    it("assigns category based on pathname", () => {
      const result = buildCanonicalLogLine(
        defaultBuildParams({ pathname: "/api/auth/login" }),
      );

      expect(result.category).toBe("api:auth");
    });
  });
});

// ---------------------------------------------------------------------------
// formatCanonicalLog
// ---------------------------------------------------------------------------

describe("formatCanonicalLog", () => {
  const originalEnv = { ...process.env };

  afterEach(() => {
    process.env.NODE_ENV = originalEnv.NODE_ENV;
    process.env.LOG_SERVICE_NAME = originalEnv.LOG_SERVICE_NAME;
  });

  it('sets level to "error" for status >= 500', () => {
    const output = JSON.parse(
      formatCanonicalLog(defaultLogLine({ statusCode: 500 })),
    );
    expect(output.level).toBe("error");
  });

  it('sets level to "error" for status 503', () => {
    const output = JSON.parse(
      formatCanonicalLog(defaultLogLine({ statusCode: 503 })),
    );
    expect(output.level).toBe("error");
  });

  it('sets level to "warn" for status 404', () => {
    const output = JSON.parse(
      formatCanonicalLog(defaultLogLine({ statusCode: 404 })),
    );
    expect(output.level).toBe("warn");
  });

  it('sets level to "warn" for status 400', () => {
    const output = JSON.parse(
      formatCanonicalLog(defaultLogLine({ statusCode: 400 })),
    );
    expect(output.level).toBe("warn");
  });

  it('sets level to "warn" for status 429 (rate limited)', () => {
    const output = JSON.parse(
      formatCanonicalLog(defaultLogLine({ statusCode: 429 })),
    );
    expect(output.level).toBe("warn");
  });

  it('sets level to "warn" for status 499 (upper boundary)', () => {
    const output = JSON.parse(
      formatCanonicalLog(defaultLogLine({ statusCode: 499 })),
    );
    expect(output.level).toBe("warn");
  });

  it('sets level to "info" for status 200', () => {
    const output = JSON.parse(
      formatCanonicalLog(defaultLogLine({ statusCode: 200 })),
    );
    expect(output.level).toBe("info");
  });

  it('sets level to "info" for status 201', () => {
    const output = JSON.parse(
      formatCanonicalLog(defaultLogLine({ statusCode: 201 })),
    );
    expect(output.level).toBe("info");
  });

  it('sets level to "info" for status 301 (redirect)', () => {
    const output = JSON.parse(
      formatCanonicalLog(defaultLogLine({ statusCode: 301 })),
    );
    expect(output.level).toBe("info");
  });

  it('includes type="canonical"', () => {
    const output = JSON.parse(formatCanonicalLog(defaultLogLine()));
    expect(output.type).toBe("canonical");
  });

  it("includes correctly formatted message", () => {
    const output = JSON.parse(
      formatCanonicalLog(
        defaultLogLine({
          method: "POST",
          path: "/api/auth/login",
          statusCode: 200,
          durationMs: 42,
        }),
      ),
    );
    expect(output.message).toBe("POST /api/auth/login 200 42ms");
  });

  it('defaults service name to "mawidi-site" when LOG_SERVICE_NAME is not set', () => {
    delete process.env.LOG_SERVICE_NAME;

    const output = JSON.parse(formatCanonicalLog(defaultLogLine()));
    expect(output.service).toBe("mawidi-site");
  });

  it("uses custom LOG_SERVICE_NAME from env", () => {
    process.env.LOG_SERVICE_NAME = "mawidi-api-gateway";

    const output = JSON.parse(formatCanonicalLog(defaultLogLine()));
    expect(output.service).toBe("mawidi-api-gateway");
  });

  it("includes environment from NODE_ENV", () => {
    process.env.NODE_ENV = "test";

    const output = JSON.parse(formatCanonicalLog(defaultLogLine()));
    expect(output.environment).toBe("test");
  });

  it("spreads all CanonicalLogLine fields into the output", () => {
    const line = defaultLogLine({
      correlationId: "trace-999",
      requestId: "req-111",
      rateLimited: true,
      responseSize: 2048,
    });

    const output = JSON.parse(formatCanonicalLog(line));

    expect(output.correlationId).toBe("trace-999");
    expect(output.requestId).toBe("req-111");
    expect(output.rateLimited).toBe(true);
    expect(output.responseSize).toBe(2048);
    expect(output.timestamp).toBe("2026-02-23T12:00:00.000Z");
  });

  it("returns a valid JSON string", () => {
    const raw = formatCanonicalLog(defaultLogLine());

    expect(() => JSON.parse(raw)).not.toThrow();
  });
});
