/**
 * Signature Service Tests
 *
 * Tests for message signing and verification.
 */

import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { SignatureService } from "./signature.service";
import { FederationService } from "./federation.service";
import { generateKeyPairSync } from "crypto";
import type Redis from "ioredis";

describe("SignatureService", () => {
  let service: SignatureService;
  let mockFederationService: Partial<FederationService>;
  let mockRedis: Partial<Redis>;

  // Test keypair
  const { publicKey, privateKey } = generateKeyPairSync("rsa", {
    modulusLength: 2048,
    publicKeyEncoding: { type: "spki", format: "pem" },
    privateKeyEncoding: { type: "pkcs8", format: "pem" },
  });

  beforeEach(async () => {
    mockFederationService = {
      getInstanceIdentity: vi.fn().mockResolvedValue({
        id: "test-id",
        instanceId: "instance-123",
        name: "Test Instance",
        url: "https://test.example.com",
        publicKey,
        privateKey,
        capabilities: {},
        metadata: {},
        createdAt: new Date(),
        updatedAt: new Date(),
      }),
    };

    mockRedis = {
      get: vi.fn().mockResolvedValue(null),
      set: vi.fn().mockResolvedValue("OK"),
      setex: vi.fn().mockResolvedValue("OK"),
    };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        SignatureService,
        {
          provide: FederationService,
          useValue: mockFederationService,
        },
        {
          provide: "REDIS_CLIENT",
          useValue: mockRedis,
        },
      ],
    }).compile();

    service = module.get<SignatureService>(SignatureService);
  });

  it("should be defined", () => {
    expect(service).toBeDefined();
  });

  describe("sign", () => {
    it("should create a valid signature for a message", () => {
      const message = {
        instanceId: "instance-123",
        timestamp: Date.now(),
        data: "test data",
      };

      const signature = service.sign(message, privateKey);

      expect(signature).toBeDefined();
      expect(typeof signature).toBe("string");
      expect(signature.length).toBeGreaterThan(0);
    });

    it("should create different signatures for different messages", () => {
      const message1 = { data: "message 1", timestamp: 1 };
      const message2 = { data: "message 2", timestamp: 2 };

      const signature1 = service.sign(message1, privateKey);
      const signature2 = service.sign(message2, privateKey);

      expect(signature1).not.toBe(signature2);
    });

    it("should create consistent signatures for the same message", () => {
      const message = { data: "test", timestamp: 12345 };

      const signature1 = service.sign(message, privateKey);
      const signature2 = service.sign(message, privateKey);

      // RSA signatures are deterministic for the same input
      expect(signature1).toBe(signature2);
    });
  });

  describe("verify", () => {
    it("should verify a valid signature", () => {
      const message = {
        instanceId: "instance-123",
        timestamp: Date.now(),
        data: "test data",
      };

      const signature = service.sign(message, privateKey);
      const result = service.verify(message, signature, publicKey);

      expect(result.valid).toBe(true);
      expect(result.error).toBeUndefined();
    });

    it("should reject an invalid signature", () => {
      const message = {
        instanceId: "instance-123",
        timestamp: Date.now(),
        data: "test data",
      };

      const invalidSignature = "invalid-signature-data";
      const result = service.verify(message, invalidSignature, publicKey);

      expect(result.valid).toBe(false);
      expect(result.error).toBeDefined();
    });

    it("should reject a tampered message", () => {
      const originalMessage = {
        instanceId: "instance-123",
        timestamp: Date.now(),
        data: "original data",
      };

      const signature = service.sign(originalMessage, privateKey);

      const tamperedMessage = {
        ...originalMessage,
        data: "tampered data",
      };

      const result = service.verify(tamperedMessage, signature, publicKey);

      expect(result.valid).toBe(false);
      expect(result.error).toBeDefined();
    });

    it("should reject a signature from wrong key", () => {
      const message = { data: "test" };

      // Generate a different keypair
      const { publicKey: wrongPublicKey, privateKey: wrongPrivateKey } = generateKeyPairSync(
        "rsa",
        {
          modulusLength: 2048,
          publicKeyEncoding: { type: "spki", format: "pem" },
          privateKeyEncoding: { type: "pkcs8", format: "pem" },
        }
      );

      const signature = service.sign(message, wrongPrivateKey);
      const result = service.verify(message, signature, publicKey);

      expect(result.valid).toBe(false);
      expect(result.error).toBeDefined();
    });
  });

  describe("validateTimestamp", () => {
    it("should accept recent timestamps", () => {
      const recentTimestamp = Date.now();
      const result = service.validateTimestamp(recentTimestamp);

      expect(result).toBe(true);
    });

    it("should accept timestamps within 60 seconds", () => {
      const fiftySecondsAgo = Date.now() - 50 * 1000;
      const result = service.validateTimestamp(fiftySecondsAgo);

      expect(result).toBe(true);
    });

    it("should reject timestamps older than 60 seconds", () => {
      const twoMinutesAgo = Date.now() - 2 * 60 * 1000;
      const result = service.validateTimestamp(twoMinutesAgo);

      expect(result).toBe(false);
    });

    it("should reject future timestamps beyond tolerance", () => {
      const farFuture = Date.now() + 10 * 60 * 1000;
      const result = service.validateTimestamp(farFuture);

      expect(result).toBe(false);
    });

    it("should accept slightly future timestamps (clock skew tolerance)", () => {
      const slightlyFuture = Date.now() + 30 * 1000; // 30 seconds
      const result = service.validateTimestamp(slightlyFuture);

      expect(result).toBe(true);
    });
  });

  describe("signMessage", () => {
    it("should sign a message with instance private key", async () => {
      const message = {
        instanceId: "instance-123",
        timestamp: Date.now(),
        data: "test",
      };

      const signature = await service.signMessage(message);

      expect(signature).toBeDefined();
      expect(typeof signature).toBe("string");
      expect(signature.length).toBeGreaterThan(0);
    });

    it("should create verifiable signatures with instance keys", async () => {
      const message = {
        instanceId: "instance-123",
        timestamp: Date.now(),
      };

      const signature = await service.signMessage(message);
      const result = service.verify(message, signature, publicKey);

      expect(result.valid).toBe(true);
    });
  });

  describe("verifyConnectionRequest", () => {
    it("should verify a valid connection request", async () => {
      const timestamp = Date.now();
      const request = {
        instanceId: "instance-123",
        instanceUrl: "https://test.example.com",
        publicKey,
        capabilities: { supportsQuery: true },
        timestamp,
      };

      const signature = service.sign(request, privateKey);
      const signedRequest = { ...request, signature };

      const result = await service.verifyConnectionRequest(signedRequest);

      expect(result.valid).toBe(true);
      expect(result.error).toBeUndefined();
      expect(mockRedis.setex).toHaveBeenCalled();
    });

    it("should reject request with invalid signature", async () => {
      const request = {
        instanceId: "instance-123",
        instanceUrl: "https://test.example.com",
        publicKey,
        capabilities: {},
        timestamp: Date.now(),
        signature: "invalid-signature",
      };

      const result = await service.verifyConnectionRequest(request);

      expect(result.valid).toBe(false);
      expect(result.error).toContain("signature");
    });

    it("should reject request with expired timestamp", async () => {
      const expiredTimestamp = Date.now() - 10 * 60 * 1000; // 10 minutes ago
      const request = {
        instanceId: "instance-123",
        instanceUrl: "https://test.example.com",
        publicKey,
        capabilities: {},
        timestamp: expiredTimestamp,
      };

      const signature = service.sign(request, privateKey);
      const signedRequest = { ...request, signature };

      const result = await service.verifyConnectionRequest(signedRequest);

      expect(result.valid).toBe(false);
      expect(result.error).toContain("timestamp");
    });
  });

  describe("replay attack prevention", () => {
    it("should reject replayed message with same signature", async () => {
      const timestamp = Date.now();
      const request = {
        instanceId: "instance-123",
        instanceUrl: "https://test.example.com",
        publicKey,
        capabilities: {},
        timestamp,
      };

      const signature = service.sign(request, privateKey);
      const signedRequest = { ...request, signature };

      // First request should succeed
      const result1 = await service.verifyConnectionRequest(signedRequest);
      expect(result1.valid).toBe(true);

      // Mock Redis to indicate nonce was already used
      mockRedis.get = vi.fn().mockResolvedValue("1");

      // Second request with same signature should be rejected
      const result2 = await service.verifyConnectionRequest(signedRequest);
      expect(result2.valid).toBe(false);
      expect(result2.error).toContain("replay");
    });

    it("should store nonce with 60 second TTL", async () => {
      const timestamp = Date.now();
      const request = {
        instanceId: "instance-123",
        instanceUrl: "https://test.example.com",
        publicKey,
        capabilities: {},
        timestamp,
      };

      const signature = service.sign(request, privateKey);
      const signedRequest = { ...request, signature };

      await service.verifyConnectionRequest(signedRequest);

      expect(mockRedis.setex).toHaveBeenCalledWith(expect.stringContaining("nonce:"), 60, "1");
    });

    it("should allow different messages with different signatures", async () => {
      const timestamp1 = Date.now();
      const request1 = {
        instanceId: "instance-123",
        instanceUrl: "https://test.example.com",
        publicKey,
        capabilities: {},
        timestamp: timestamp1,
      };

      const signature1 = service.sign(request1, privateKey);
      const signedRequest1 = { ...request1, signature: signature1 };

      const result1 = await service.verifyConnectionRequest(signedRequest1);
      expect(result1.valid).toBe(true);

      // Different timestamp creates different signature
      const timestamp2 = Date.now() + 1;
      const request2 = {
        instanceId: "instance-123",
        instanceUrl: "https://test.example.com",
        publicKey,
        capabilities: {},
        timestamp: timestamp2,
      };

      const signature2 = service.sign(request2, privateKey);
      const signedRequest2 = { ...request2, signature: signature2 };

      // Reset mock to simulate nonce not found
      mockRedis.get = vi.fn().mockResolvedValue(null);

      const result2 = await service.verifyConnectionRequest(signedRequest2);
      expect(result2.valid).toBe(true);
    });
  });
});
