/**
 * Identity Linking Service Tests
 *
 * Tests for cross-instance identity verification and mapping.
 */

import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { IdentityLinkingService } from "./identity-linking.service";
import { OIDCService } from "./oidc.service";
import { SignatureService } from "./signature.service";
import { FederationAuditService } from "./audit.service";
import { PrismaService } from "../prisma/prisma.service";
import type {
  IdentityVerificationRequest,
  CreateIdentityMappingDto,
  UpdateIdentityMappingDto,
} from "./types/identity-linking.types";
import type { FederatedIdentity } from "./types/oidc.types";

describe("IdentityLinkingService", () => {
  let service: IdentityLinkingService;
  let oidcService: OIDCService;
  let signatureService: SignatureService;
  let auditService: FederationAuditService;
  let prismaService: PrismaService;

  const mockFederatedIdentity: FederatedIdentity = {
    id: "identity-id",
    localUserId: "local-user-id",
    remoteUserId: "remote-user-id",
    remoteInstanceId: "remote-instance-id",
    oidcSubject: "oidc-subject",
    email: "user@example.com",
    metadata: {},
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  beforeEach(async () => {
    const mockOIDCService = {
      linkFederatedIdentity: vi.fn(),
      getFederatedIdentity: vi.fn(),
      getUserFederatedIdentities: vi.fn(),
      revokeFederatedIdentity: vi.fn(),
      validateToken: vi.fn(),
    };

    const mockSignatureService = {
      verifyMessage: vi.fn(),
      validateTimestamp: vi.fn(),
    };

    const mockAuditService = {
      logIdentityVerification: vi.fn(),
      logIdentityLinking: vi.fn(),
      logIdentityRevocation: vi.fn(),
    };

    const mockPrismaService = {
      federatedIdentity: {
        findUnique: vi.fn(),
        findFirst: vi.fn(),
        update: vi.fn(),
      },
    };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        IdentityLinkingService,
        { provide: OIDCService, useValue: mockOIDCService },
        { provide: SignatureService, useValue: mockSignatureService },
        { provide: FederationAuditService, useValue: mockAuditService },
        { provide: PrismaService, useValue: mockPrismaService },
      ],
    }).compile();

    service = module.get<IdentityLinkingService>(IdentityLinkingService);
    oidcService = module.get(OIDCService);
    signatureService = module.get(SignatureService);
    auditService = module.get(FederationAuditService);
    prismaService = module.get(PrismaService);
  });

  afterEach(() => {
    vi.clearAllMocks();
  });

  describe("verifyIdentity", () => {
    it("should verify identity with valid signature and token", async () => {
      const request: IdentityVerificationRequest = {
        localUserId: "local-user-id",
        remoteUserId: "remote-user-id",
        remoteInstanceId: "remote-instance-id",
        oidcToken: "valid-token",
        timestamp: Date.now(),
        signature: "valid-signature",
      };

      signatureService.validateTimestamp.mockReturnValue(true);
      signatureService.verifyMessage.mockResolvedValue({ valid: true });
      oidcService.validateToken.mockReturnValue({
        valid: true,
        userId: "remote-user-id",
        instanceId: "remote-instance-id",
        email: "user@example.com",
      });
      oidcService.getFederatedIdentity.mockResolvedValue(mockFederatedIdentity);

      const result = await service.verifyIdentity(request);

      expect(result.verified).toBe(true);
      expect(result.localUserId).toBe("local-user-id");
      expect(result.remoteUserId).toBe("remote-user-id");
      expect(result.remoteInstanceId).toBe("remote-instance-id");
      expect(signatureService.validateTimestamp).toHaveBeenCalledWith(request.timestamp);
      expect(signatureService.verifyMessage).toHaveBeenCalled();
      expect(oidcService.validateToken).toHaveBeenCalledWith("valid-token", "remote-instance-id");
      expect(auditService.logIdentityVerification).toHaveBeenCalled();
    });

    it("should reject identity with invalid signature", async () => {
      const request: IdentityVerificationRequest = {
        localUserId: "local-user-id",
        remoteUserId: "remote-user-id",
        remoteInstanceId: "remote-instance-id",
        oidcToken: "valid-token",
        timestamp: Date.now(),
        signature: "invalid-signature",
      };

      signatureService.validateTimestamp.mockReturnValue(true);
      signatureService.verifyMessage.mockResolvedValue({
        valid: false,
        error: "Invalid signature",
      });

      const result = await service.verifyIdentity(request);

      expect(result.verified).toBe(false);
      expect(result.error).toContain("Invalid signature");
      expect(oidcService.validateToken).not.toHaveBeenCalled();
    });

    it("should reject identity with expired timestamp", async () => {
      const request: IdentityVerificationRequest = {
        localUserId: "local-user-id",
        remoteUserId: "remote-user-id",
        remoteInstanceId: "remote-instance-id",
        oidcToken: "valid-token",
        timestamp: Date.now() - 10 * 60 * 1000, // 10 minutes ago
        signature: "valid-signature",
      };

      signatureService.validateTimestamp.mockReturnValue(false);

      const result = await service.verifyIdentity(request);

      expect(result.verified).toBe(false);
      expect(result.error).toContain("expired");
      expect(signatureService.verifyMessage).not.toHaveBeenCalled();
    });

    it("should reject identity with invalid OIDC token", async () => {
      const request: IdentityVerificationRequest = {
        localUserId: "local-user-id",
        remoteUserId: "remote-user-id",
        remoteInstanceId: "remote-instance-id",
        oidcToken: "invalid-token",
        timestamp: Date.now(),
        signature: "valid-signature",
      };

      signatureService.validateTimestamp.mockReturnValue(true);
      signatureService.verifyMessage.mockResolvedValue({ valid: true });
      oidcService.validateToken.mockReturnValue({
        valid: false,
        error: "Invalid token",
      });

      const result = await service.verifyIdentity(request);

      expect(result.verified).toBe(false);
      expect(result.error).toContain("Invalid token");
    });

    it("should reject identity if mapping does not exist", async () => {
      const request: IdentityVerificationRequest = {
        localUserId: "local-user-id",
        remoteUserId: "remote-user-id",
        remoteInstanceId: "remote-instance-id",
        oidcToken: "valid-token",
        timestamp: Date.now(),
        signature: "valid-signature",
      };

      signatureService.validateTimestamp.mockReturnValue(true);
      signatureService.verifyMessage.mockResolvedValue({ valid: true });
      oidcService.validateToken.mockReturnValue({
        valid: true,
        userId: "remote-user-id",
        instanceId: "remote-instance-id",
      });
      oidcService.getFederatedIdentity.mockResolvedValue(null);

      const result = await service.verifyIdentity(request);

      expect(result.verified).toBe(false);
      expect(result.error).toContain("not found");
    });
  });

  describe("resolveLocalIdentity", () => {
    it("should resolve remote user to local user", async () => {
      prismaService.federatedIdentity.findFirst.mockResolvedValue(mockFederatedIdentity as never);

      const result = await service.resolveLocalIdentity("remote-instance-id", "remote-user-id");

      expect(result).not.toBeNull();
      expect(result?.localUserId).toBe("local-user-id");
      expect(result?.remoteUserId).toBe("remote-user-id");
      expect(result?.email).toBe("user@example.com");
    });

    it("should return null when mapping not found", async () => {
      prismaService.federatedIdentity.findFirst.mockResolvedValue(null);

      const result = await service.resolveLocalIdentity("remote-instance-id", "unknown-user-id");

      expect(result).toBeNull();
    });
  });

  describe("resolveRemoteIdentity", () => {
    it("should resolve local user to remote user", async () => {
      oidcService.getFederatedIdentity.mockResolvedValue(mockFederatedIdentity);

      const result = await service.resolveRemoteIdentity("local-user-id", "remote-instance-id");

      expect(result).not.toBeNull();
      expect(result?.remoteUserId).toBe("remote-user-id");
      expect(result?.localUserId).toBe("local-user-id");
    });

    it("should return null when mapping not found", async () => {
      oidcService.getFederatedIdentity.mockResolvedValue(null);

      const result = await service.resolveRemoteIdentity("unknown-user-id", "remote-instance-id");

      expect(result).toBeNull();
    });
  });

  describe("createIdentityMapping", () => {
    it("should create identity mapping with valid data", async () => {
      const dto: CreateIdentityMappingDto = {
        remoteInstanceId: "remote-instance-id",
        remoteUserId: "remote-user-id",
        oidcSubject: "oidc-subject",
        email: "user@example.com",
        metadata: { source: "manual" },
      };

      oidcService.linkFederatedIdentity.mockResolvedValue(mockFederatedIdentity);

      const result = await service.createIdentityMapping("local-user-id", dto);

      expect(result).toEqual(mockFederatedIdentity);
      expect(oidcService.linkFederatedIdentity).toHaveBeenCalledWith(
        "local-user-id",
        "remote-user-id",
        "remote-instance-id",
        "oidc-subject",
        "user@example.com",
        { source: "manual" }
      );
      expect(auditService.logIdentityLinking).toHaveBeenCalled();
    });

    it("should validate OIDC token if provided", async () => {
      const dto: CreateIdentityMappingDto = {
        remoteInstanceId: "remote-instance-id",
        remoteUserId: "remote-user-id",
        oidcSubject: "oidc-subject",
        email: "user@example.com",
        oidcToken: "valid-token",
      };

      oidcService.validateToken.mockReturnValue({ valid: true });
      oidcService.linkFederatedIdentity.mockResolvedValue(mockFederatedIdentity);

      await service.createIdentityMapping("local-user-id", dto);

      expect(oidcService.validateToken).toHaveBeenCalledWith("valid-token", "remote-instance-id");
    });

    it("should throw error if OIDC token is invalid", async () => {
      const dto: CreateIdentityMappingDto = {
        remoteInstanceId: "remote-instance-id",
        remoteUserId: "remote-user-id",
        oidcSubject: "oidc-subject",
        email: "user@example.com",
        oidcToken: "invalid-token",
      };

      oidcService.validateToken.mockReturnValue({
        valid: false,
        error: "Invalid token",
      });

      await expect(service.createIdentityMapping("local-user-id", dto)).rejects.toThrow(
        "Invalid OIDC token"
      );
    });
  });

  describe("updateIdentityMapping", () => {
    it("should update identity mapping metadata", async () => {
      const dto: UpdateIdentityMappingDto = {
        metadata: { updated: true },
      };

      const updatedIdentity = { ...mockFederatedIdentity, metadata: { updated: true } };
      prismaService.federatedIdentity.findUnique.mockResolvedValue(mockFederatedIdentity as never);
      prismaService.federatedIdentity.update.mockResolvedValue(updatedIdentity as never);

      const result = await service.updateIdentityMapping(
        "local-user-id",
        "remote-instance-id",
        dto
      );

      expect(result.metadata).toEqual({ updated: true });
      expect(prismaService.federatedIdentity.update).toHaveBeenCalled();
    });

    it("should throw error if mapping not found", async () => {
      const dto: UpdateIdentityMappingDto = {
        metadata: { updated: true },
      };

      prismaService.federatedIdentity.findUnique.mockResolvedValue(null);

      await expect(
        service.updateIdentityMapping("unknown-user-id", "remote-instance-id", dto)
      ).rejects.toThrow("not found");
    });
  });

  describe("validateIdentityMapping", () => {
    it("should validate existing identity mapping", async () => {
      oidcService.getFederatedIdentity.mockResolvedValue(mockFederatedIdentity);

      const result = await service.validateIdentityMapping("local-user-id", "remote-instance-id");

      expect(result.valid).toBe(true);
      expect(result.localUserId).toBe("local-user-id");
      expect(result.remoteUserId).toBe("remote-user-id");
    });

    it("should return invalid if mapping not found", async () => {
      oidcService.getFederatedIdentity.mockResolvedValue(null);

      const result = await service.validateIdentityMapping("unknown-user-id", "remote-instance-id");

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

  describe("listUserIdentities", () => {
    it("should list all federated identities for a user", async () => {
      const identities = [mockFederatedIdentity];
      oidcService.getUserFederatedIdentities.mockResolvedValue(identities);

      const result = await service.listUserIdentities("local-user-id");

      expect(result).toEqual(identities);
      expect(oidcService.getUserFederatedIdentities).toHaveBeenCalledWith("local-user-id");
    });

    it("should return empty array if user has no federated identities", async () => {
      oidcService.getUserFederatedIdentities.mockResolvedValue([]);

      const result = await service.listUserIdentities("local-user-id");

      expect(result).toEqual([]);
    });
  });

  describe("revokeIdentityMapping", () => {
    it("should revoke identity mapping", async () => {
      oidcService.revokeFederatedIdentity.mockResolvedValue(undefined);

      await service.revokeIdentityMapping("local-user-id", "remote-instance-id");

      expect(oidcService.revokeFederatedIdentity).toHaveBeenCalledWith(
        "local-user-id",
        "remote-instance-id"
      );
      expect(auditService.logIdentityRevocation).toHaveBeenCalled();
    });
  });
});
