/**
 * CSRF Service Tests
 *
 * Tests CSRF token generation and validation with session binding.
 */

import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { CsrfService } from "./csrf.service";

describe("CsrfService", () => {
  let service: CsrfService;
  const originalEnv = process.env;

  beforeEach(() => {
    process.env = { ...originalEnv };
    // Set a consistent secret for tests
    process.env.CSRF_SECRET = "test-secret-key-0123456789abcdef0123456789abcdef";
    service = new CsrfService();
    service.onModuleInit();
  });

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

  describe("onModuleInit", () => {
    it("should initialize with configured secret", () => {
      const testService = new CsrfService();
      process.env.CSRF_SECRET = "configured-secret";
      expect(() => testService.onModuleInit()).not.toThrow();
    });

    it("should throw in production without CSRF_SECRET", () => {
      const testService = new CsrfService();
      process.env.NODE_ENV = "production";
      delete process.env.CSRF_SECRET;
      expect(() => testService.onModuleInit()).toThrow(
        "CSRF_SECRET environment variable is required in production"
      );
    });

    it("should generate random secret in development without CSRF_SECRET", () => {
      const testService = new CsrfService();
      process.env.NODE_ENV = "development";
      delete process.env.CSRF_SECRET;
      expect(() => testService.onModuleInit()).not.toThrow();
    });
  });

  describe("generateToken", () => {
    it("should generate a token with random:hmac format", () => {
      const token = service.generateToken("user-123");

      expect(token).toContain(":");
      const parts = token.split(":");
      expect(parts).toHaveLength(2);
    });

    it("should generate 64-char hex random part (32 bytes)", () => {
      const token = service.generateToken("user-123");
      const randomPart = token.split(":")[0];

      expect(randomPart).toHaveLength(64);
      expect(/^[0-9a-f]{64}$/.test(randomPart as string)).toBe(true);
    });

    it("should generate 64-char hex HMAC (SHA-256)", () => {
      const token = service.generateToken("user-123");
      const hmacPart = token.split(":")[1];

      expect(hmacPart).toHaveLength(64);
      expect(/^[0-9a-f]{64}$/.test(hmacPart as string)).toBe(true);
    });

    it("should generate unique tokens on each call", () => {
      const token1 = service.generateToken("user-123");
      const token2 = service.generateToken("user-123");

      expect(token1).not.toBe(token2);
    });

    it("should generate different HMACs for different sessions", () => {
      const token1 = service.generateToken("user-123");
      const token2 = service.generateToken("user-456");

      const hmac1 = token1.split(":")[1];
      const hmac2 = token2.split(":")[1];

      // Even with same random part, HMACs would differ due to session binding
      // But since random parts differ, this just confirms they're different tokens
      expect(hmac1).not.toBe(hmac2);
    });
  });

  describe("validateToken", () => {
    it("should validate a token for the correct session", () => {
      const sessionId = "user-123";
      const token = service.generateToken(sessionId);

      expect(service.validateToken(token, sessionId)).toBe(true);
    });

    it("should reject a token for a different session", () => {
      const token = service.generateToken("user-123");

      expect(service.validateToken(token, "user-456")).toBe(false);
    });

    it("should reject empty token", () => {
      expect(service.validateToken("", "user-123")).toBe(false);
    });

    it("should reject empty session ID", () => {
      const token = service.generateToken("user-123");
      expect(service.validateToken(token, "")).toBe(false);
    });

    it("should reject token without colon separator", () => {
      expect(service.validateToken("invalidtoken", "user-123")).toBe(false);
    });

    it("should reject token with empty random part", () => {
      expect(service.validateToken(":somehash", "user-123")).toBe(false);
    });

    it("should reject token with empty HMAC part", () => {
      expect(service.validateToken("somerandom:", "user-123")).toBe(false);
    });

    it("should reject token with invalid hex in random part", () => {
      expect(
        service.validateToken(
          "invalid-hex-here-not-64-chars:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
          "user-123"
        )
      ).toBe(false);
    });

    it("should reject token with invalid hex in HMAC part", () => {
      expect(
        service.validateToken(
          "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef:not-valid-hex",
          "user-123"
        )
      ).toBe(false);
    });

    it("should reject token with tampered HMAC", () => {
      const token = service.generateToken("user-123");
      const parts = token.split(":");
      // Tamper with the HMAC
      const tamperedToken = `${parts[0]}:0000000000000000000000000000000000000000000000000000000000000000`;

      expect(service.validateToken(tamperedToken, "user-123")).toBe(false);
    });

    it("should reject token with tampered random part", () => {
      const token = service.generateToken("user-123");
      const parts = token.split(":");
      // Tamper with the random part
      const tamperedToken = `0000000000000000000000000000000000000000000000000000000000000000:${parts[1]}`;

      expect(service.validateToken(tamperedToken, "user-123")).toBe(false);
    });
  });

  describe("session binding security", () => {
    it("should bind token to specific session", () => {
      const token = service.generateToken("session-A");

      // Token valid for session-A
      expect(service.validateToken(token, "session-A")).toBe(true);

      // Token invalid for any other session
      expect(service.validateToken(token, "session-B")).toBe(false);
      expect(service.validateToken(token, "session-C")).toBe(false);
      expect(service.validateToken(token, "")).toBe(false);
    });

    it("should not allow token reuse across sessions", () => {
      const userAToken = service.generateToken("user-A");
      const userBToken = service.generateToken("user-B");

      // Each token only valid for its own session
      expect(service.validateToken(userAToken, "user-A")).toBe(true);
      expect(service.validateToken(userAToken, "user-B")).toBe(false);

      expect(service.validateToken(userBToken, "user-B")).toBe(true);
      expect(service.validateToken(userBToken, "user-A")).toBe(false);
    });

    it("should use different secrets to generate different tokens", () => {
      // Generate token with current secret
      const token1 = service.generateToken("user-123");

      // Create new service with different secret
      process.env.CSRF_SECRET = "different-secret-key-abcdef0123456789";
      const service2 = new CsrfService();
      service2.onModuleInit();

      // Token from service1 should not validate with service2
      expect(service2.validateToken(token1, "user-123")).toBe(false);

      // But service2's own tokens should validate
      const token2 = service2.generateToken("user-123");
      expect(service2.validateToken(token2, "user-123")).toBe(true);
    });
  });
});
