/**
 * Identity Linking Controller Tests
 *
 * Integration tests for identity linking API endpoints.
 */

import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { ExecutionContext } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { IdentityLinkingController } from "./identity-linking.controller";
import { IdentityLinkingService } from "./identity-linking.service";
import { IdentityResolutionService } from "./identity-resolution.service";
import { AuthGuard } from "../auth/guards/auth.guard";
import type { FederatedIdentity } from "./types/oidc.types";
import type {
  CreateIdentityMappingDto,
  UpdateIdentityMappingDto,
  VerifyIdentityDto,
  ResolveIdentityDto,
  BulkResolveIdentityDto,
} from "./dto/identity-linking.dto";

describe("IdentityLinkingController", () => {
  let controller: IdentityLinkingController;
  let identityLinkingService: IdentityLinkingService;
  let identityResolutionService: IdentityResolutionService;

  const mockIdentity: 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(),
  };

  const mockUser = {
    id: "local-user-id",
    email: "user@example.com",
    name: "Test User",
  };

  beforeEach(async () => {
    const mockIdentityLinkingService = {
      verifyIdentity: vi.fn(),
      createIdentityMapping: vi.fn(),
      updateIdentityMapping: vi.fn(),
      validateIdentityMapping: vi.fn(),
      listUserIdentities: vi.fn(),
      revokeIdentityMapping: vi.fn(),
    };

    const mockIdentityResolutionService = {
      resolveIdentity: vi.fn(),
      reverseResolveIdentity: vi.fn(),
      bulkResolveIdentities: vi.fn(),
    };

    const mockAuthGuard = {
      canActivate: (context: ExecutionContext) => {
        const request = context.switchToHttp().getRequest();
        request.user = mockUser;
        return true;
      },
    };

    const module: TestingModule = await Test.createTestingModule({
      controllers: [IdentityLinkingController],
      providers: [
        { provide: IdentityLinkingService, useValue: mockIdentityLinkingService },
        { provide: IdentityResolutionService, useValue: mockIdentityResolutionService },
        { provide: Reflector, useValue: { getAllAndOverride: vi.fn(() => []) } },
      ],
    })
      .overrideGuard(AuthGuard)
      .useValue(mockAuthGuard)
      .compile();

    controller = module.get<IdentityLinkingController>(IdentityLinkingController);
    identityLinkingService = module.get(IdentityLinkingService);
    identityResolutionService = module.get(IdentityResolutionService);
  });

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

  describe("POST /identity/verify", () => {
    it("should have AuthGuard and Throttle decorators applied", () => {
      // Verify that the endpoint has proper guards and rate limiting
      const verifyMetadata = Reflect.getMetadata(
        "__guards__",
        IdentityLinkingController.prototype.verifyIdentity
      );
      expect(verifyMetadata).toBeDefined();
    });

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

      identityLinkingService.verifyIdentity.mockResolvedValue({
        verified: true,
        localUserId: "local-user-id",
        remoteUserId: "remote-user-id",
        remoteInstanceId: "remote-instance-id",
        email: "user@example.com",
      });

      const result = await controller.verifyIdentity(dto);

      expect(result.verified).toBe(true);
      expect(result.localUserId).toBe("local-user-id");
      expect(identityLinkingService.verifyIdentity).toHaveBeenCalledWith(dto);
    });

    it("should return verification failure", async () => {
      const dto: VerifyIdentityDto = {
        localUserId: "local-user-id",
        remoteUserId: "remote-user-id",
        remoteInstanceId: "remote-instance-id",
        oidcToken: "invalid-token",
        timestamp: Date.now(),
        signature: "invalid-signature",
      };

      identityLinkingService.verifyIdentity.mockResolvedValue({
        verified: false,
        error: "Invalid signature",
      });

      const result = await controller.verifyIdentity(dto);

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

  describe("POST /identity/resolve", () => {
    it("should resolve remote user to local user", async () => {
      const dto: ResolveIdentityDto = {
        remoteInstanceId: "remote-instance-id",
        remoteUserId: "remote-user-id",
      };

      identityResolutionService.resolveIdentity.mockResolvedValue({
        found: true,
        localUserId: "local-user-id",
        remoteUserId: "remote-user-id",
        remoteInstanceId: "remote-instance-id",
        email: "user@example.com",
      });

      const result = await controller.resolveIdentity(dto);

      expect(result.found).toBe(true);
      expect(result.localUserId).toBe("local-user-id");
      expect(identityResolutionService.resolveIdentity).toHaveBeenCalledWith(
        "remote-instance-id",
        "remote-user-id"
      );
    });

    it("should return not found when mapping does not exist", async () => {
      const dto: ResolveIdentityDto = {
        remoteInstanceId: "remote-instance-id",
        remoteUserId: "unknown-user-id",
      };

      identityResolutionService.resolveIdentity.mockResolvedValue({
        found: false,
        remoteUserId: "unknown-user-id",
        remoteInstanceId: "remote-instance-id",
      });

      const result = await controller.resolveIdentity(dto);

      expect(result.found).toBe(false);
      expect(result.localUserId).toBeUndefined();
    });
  });

  describe("POST /identity/bulk-resolve", () => {
    it("should resolve multiple remote users", async () => {
      const dto: BulkResolveIdentityDto = {
        remoteInstanceId: "remote-instance-id",
        remoteUserIds: ["remote-user-1", "remote-user-2", "unknown-user"],
      };

      identityResolutionService.bulkResolveIdentities.mockResolvedValue({
        mappings: {
          "remote-user-1": "local-user-1",
          "remote-user-2": "local-user-2",
        },
        notFound: ["unknown-user"],
      });

      const result = await controller.bulkResolveIdentity(dto);

      expect(result.mappings).toEqual({
        "remote-user-1": "local-user-1",
        "remote-user-2": "local-user-2",
      });
      expect(result.notFound).toEqual(["unknown-user"]);
    });
  });

  describe("GET /identity/me", () => {
    it("should return current user's federated identities", async () => {
      identityLinkingService.listUserIdentities.mockResolvedValue([mockIdentity]);

      const result = await controller.getCurrentUserIdentities(mockUser);

      expect(result).toHaveLength(1);
      expect(result[0]).toEqual(mockIdentity);
      expect(identityLinkingService.listUserIdentities).toHaveBeenCalledWith("local-user-id");
    });

    it("should return empty array if no identities", async () => {
      identityLinkingService.listUserIdentities.mockResolvedValue([]);

      const result = await controller.getCurrentUserIdentities(mockUser);

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

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

      identityLinkingService.createIdentityMapping.mockResolvedValue(mockIdentity);

      const result = await controller.createIdentityMapping(mockUser, dto);

      expect(result).toEqual(mockIdentity);
      expect(identityLinkingService.createIdentityMapping).toHaveBeenCalledWith(
        "local-user-id",
        dto
      );
    });
  });

  describe("PATCH /identity/:remoteInstanceId", () => {
    it("should update identity mapping", async () => {
      const remoteInstanceId = "remote-instance-id";
      const dto: UpdateIdentityMappingDto = {
        metadata: { updated: true },
      };

      const updatedIdentity = { ...mockIdentity, metadata: { updated: true } };
      identityLinkingService.updateIdentityMapping.mockResolvedValue(updatedIdentity);

      const result = await controller.updateIdentityMapping(mockUser, remoteInstanceId, dto);

      expect(result.metadata).toEqual({ updated: true });
      expect(identityLinkingService.updateIdentityMapping).toHaveBeenCalledWith(
        "local-user-id",
        remoteInstanceId,
        dto
      );
    });
  });

  describe("DELETE /identity/:remoteInstanceId", () => {
    it("should revoke identity mapping", async () => {
      const remoteInstanceId = "remote-instance-id";

      identityLinkingService.revokeIdentityMapping.mockResolvedValue(undefined);

      const result = await controller.revokeIdentityMapping(mockUser, remoteInstanceId);

      expect(result).toEqual({ success: true });
      expect(identityLinkingService.revokeIdentityMapping).toHaveBeenCalledWith(
        "local-user-id",
        remoteInstanceId
      );
    });
  });

  describe("GET /identity/:remoteInstanceId/validate", () => {
    it("should validate existing identity mapping", async () => {
      const remoteInstanceId = "remote-instance-id";

      identityLinkingService.validateIdentityMapping.mockResolvedValue({
        valid: true,
        localUserId: "local-user-id",
        remoteUserId: "remote-user-id",
        remoteInstanceId: "remote-instance-id",
      });

      const result = await controller.validateIdentityMapping(mockUser, remoteInstanceId);

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

    it("should return invalid if mapping not found", async () => {
      const remoteInstanceId = "unknown-instance-id";

      identityLinkingService.validateIdentityMapping.mockResolvedValue({
        valid: false,
        error: "Identity mapping not found",
      });

      const result = await controller.validateIdentityMapping(mockUser, remoteInstanceId);

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