/**
 * Connection Service Tests
 *
 * Tests for federation connection management.
 */

import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { HttpService } from "@nestjs/axios";
import { ConnectionService } from "./connection.service";
import { FederationService } from "./federation.service";
import { SignatureService } from "./signature.service";
import { FederationAuditService } from "./audit.service";
import { PrismaService } from "../prisma/prisma.service";
import { FederationConnectionStatus } from "@prisma/client";
import { FederationConnection } from "@prisma/client";
import { of, throwError } from "rxjs";
import type { AxiosResponse } from "axios";

describe("ConnectionService", () => {
  let service: ConnectionService;
  let prismaService: PrismaService;
  let federationService: FederationService;
  let signatureService: SignatureService;
  let httpService: HttpService;
  let auditService: FederationAuditService;

  const mockWorkspaceId = "workspace-123";
  const mockRemoteUrl = "https://remote.example.com";
  const mockInstanceIdentity = {
    id: "local-id",
    instanceId: "local-instance-123",
    name: "Local Instance",
    url: "https://local.example.com",
    publicKey: "-----BEGIN PUBLIC KEY-----\nLOCAL\n-----END PUBLIC KEY-----",
    privateKey: "-----BEGIN PRIVATE KEY-----\nLOCAL\n-----END PRIVATE KEY-----",
    capabilities: {
      supportsQuery: true,
      supportsCommand: true,
      protocolVersion: "1.0",
    },
    metadata: {},
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  const mockRemoteIdentity = {
    id: "remote-id",
    instanceId: "remote-instance-456",
    name: "Remote Instance",
    url: mockRemoteUrl,
    publicKey: "-----BEGIN PUBLIC KEY-----\nREMOTE\n-----END PUBLIC KEY-----",
    capabilities: {
      supportsQuery: true,
      protocolVersion: "1.0",
    },
    metadata: {},
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  const mockConnection: FederationConnection = {
    id: "conn-123",
    workspaceId: mockWorkspaceId,
    remoteInstanceId: mockRemoteIdentity.instanceId,
    remoteUrl: mockRemoteUrl,
    remotePublicKey: mockRemoteIdentity.publicKey,
    remoteCapabilities: mockRemoteIdentity.capabilities,
    status: FederationConnectionStatus.PENDING,
    metadata: {},
    createdAt: new Date(),
    updatedAt: new Date(),
    connectedAt: null,
    disconnectedAt: null,
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        ConnectionService,
        {
          provide: PrismaService,
          useValue: {
            federationConnection: {
              create: vi.fn(),
              findFirst: vi.fn(),
              findUnique: vi.fn(),
              findMany: vi.fn(),
              update: vi.fn(),
              delete: vi.fn(),
              count: vi.fn(),
            },
          },
        },
        {
          provide: FederationService,
          useValue: {
            getInstanceIdentity: vi.fn().mockResolvedValue(mockInstanceIdentity),
            getPublicIdentity: vi.fn().mockResolvedValue(mockInstanceIdentity),
          },
        },
        {
          provide: SignatureService,
          useValue: {
            signMessage: vi.fn().mockResolvedValue("mock-signature"),
            verifyConnectionRequest: vi.fn().mockResolvedValue({ valid: true }),
          },
        },
        {
          provide: HttpService,
          useValue: {
            get: vi.fn(),
            post: vi.fn(),
          },
        },
        {
          provide: FederationAuditService,
          useValue: {
            logIncomingConnectionAttempt: vi.fn(),
            logIncomingConnectionCreated: vi.fn(),
            logIncomingConnectionRejected: vi.fn(),
          },
        },
      ],
    }).compile();

    service = module.get<ConnectionService>(ConnectionService);
    prismaService = module.get<PrismaService>(PrismaService);
    federationService = module.get<FederationService>(FederationService);
    signatureService = module.get<SignatureService>(SignatureService);
    httpService = module.get<HttpService>(HttpService);
    auditService = module.get<FederationAuditService>(FederationAuditService);
  });

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

  describe("initiateConnection", () => {
    it("should throw error if workspace has reached connection limit", async () => {
      const existingConnections = Array.from({ length: 100 }, (_, i) => ({
        ...mockConnection,
        id: `conn-${i}`,
      }));

      vi.spyOn(prismaService.federationConnection, "count").mockResolvedValue(100);

      await expect(service.initiateConnection(mockWorkspaceId, mockRemoteUrl)).rejects.toThrow(
        "Connection limit reached for workspace. Maximum 100 connections allowed per workspace."
      );
    });

    it("should reject connection to instance with incompatible protocol version", async () => {
      const incompatibleRemoteIdentity = {
        ...mockRemoteIdentity,
        capabilities: {
          ...mockRemoteIdentity.capabilities,
          protocolVersion: "2.0",
        },
      };

      const mockAxiosResponse: AxiosResponse = {
        data: incompatibleRemoteIdentity,
        status: 200,
        statusText: "OK",
        headers: {},
        config: {} as never,
      };

      vi.spyOn(prismaService.federationConnection, "count").mockResolvedValue(5);
      vi.spyOn(httpService, "get").mockReturnValue(of(mockAxiosResponse));

      await expect(service.initiateConnection(mockWorkspaceId, mockRemoteUrl)).rejects.toThrow(
        "Incompatible protocol version. Expected 1.0, received 2.0"
      );
    });

    it("should create a pending connection", async () => {
      const mockAxiosResponse: AxiosResponse = {
        data: mockRemoteIdentity,
        status: 200,
        statusText: "OK",
        headers: {},
        config: {} as never,
      };

      vi.spyOn(prismaService.federationConnection, "count").mockResolvedValue(5);
      vi.spyOn(httpService, "get").mockReturnValue(of(mockAxiosResponse));
      vi.spyOn(httpService, "post").mockReturnValue(
        of({ data: { accepted: true } } as AxiosResponse)
      );
      vi.spyOn(prismaService.federationConnection, "create").mockResolvedValue(mockConnection);

      const result = await service.initiateConnection(mockWorkspaceId, mockRemoteUrl);

      expect(result).toBeDefined();
      expect(result.status).toBe(FederationConnectionStatus.PENDING);
      expect(result.remoteUrl).toBe(mockRemoteUrl);
      expect(prismaService.federationConnection.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            workspaceId: mockWorkspaceId,
            remoteUrl: mockRemoteUrl,
            status: FederationConnectionStatus.PENDING,
          }),
        })
      );
    });

    it("should fetch remote instance identity", async () => {
      const mockAxiosResponse: AxiosResponse = {
        data: mockRemoteIdentity,
        status: 200,
        statusText: "OK",
        headers: {},
        config: {} as never,
      };

      vi.spyOn(prismaService.federationConnection, "count").mockResolvedValue(5);
      vi.spyOn(httpService, "get").mockReturnValue(of(mockAxiosResponse));
      vi.spyOn(httpService, "post").mockReturnValue(
        of({ data: { accepted: true } } as AxiosResponse)
      );
      vi.spyOn(prismaService.federationConnection, "create").mockResolvedValue(mockConnection);

      await service.initiateConnection(mockWorkspaceId, mockRemoteUrl);

      expect(httpService.get).toHaveBeenCalledWith(`${mockRemoteUrl}/api/v1/federation/instance`);
    });

    it("should throw error if remote instance not reachable", async () => {
      vi.spyOn(httpService, "get").mockReturnValue(throwError(() => new Error("Network error")));

      await expect(service.initiateConnection(mockWorkspaceId, mockRemoteUrl)).rejects.toThrow();
    });

    it("should send signed connection request", async () => {
      const mockAxiosResponse: AxiosResponse = {
        data: mockRemoteIdentity,
        status: 200,
        statusText: "OK",
        headers: {},
        config: {} as never,
      };

      vi.spyOn(prismaService.federationConnection, "count").mockResolvedValue(5);
      const postSpy = vi
        .spyOn(httpService, "post")
        .mockReturnValue(of({ data: { accepted: true } } as AxiosResponse));
      vi.spyOn(httpService, "get").mockReturnValue(of(mockAxiosResponse));
      vi.spyOn(prismaService.federationConnection, "create").mockResolvedValue(mockConnection);

      await service.initiateConnection(mockWorkspaceId, mockRemoteUrl);

      expect(postSpy).toHaveBeenCalledWith(
        `${mockRemoteUrl}/api/v1/federation/incoming/connect`,
        expect.objectContaining({
          instanceId: mockInstanceIdentity.instanceId,
          instanceUrl: mockInstanceIdentity.url,
          publicKey: mockInstanceIdentity.publicKey,
          signature: "mock-signature",
        })
      );
    });

    it("should delete connection and throw error if request fails", async () => {
      const mockAxiosResponse: AxiosResponse = {
        data: mockRemoteIdentity,
        status: 200,
        statusText: "OK",
        headers: {},
        config: {} as never,
      };

      vi.spyOn(prismaService.federationConnection, "count").mockResolvedValue(5);
      vi.spyOn(httpService, "get").mockReturnValue(of(mockAxiosResponse));
      vi.spyOn(httpService, "post").mockReturnValue(
        throwError(() => new Error("Connection refused"))
      );
      const createSpy = vi
        .spyOn(prismaService.federationConnection, "create")
        .mockResolvedValue(mockConnection);
      const deleteSpy = vi
        .spyOn(prismaService.federationConnection, "delete")
        .mockResolvedValue(mockConnection);

      await expect(service.initiateConnection(mockWorkspaceId, mockRemoteUrl)).rejects.toThrow(
        "Failed to initiate connection"
      );

      expect(createSpy).toHaveBeenCalled();
      expect(deleteSpy).toHaveBeenCalledWith({
        where: { id: mockConnection.id },
      });
    });
  });

  describe("acceptConnection", () => {
    it("should update connection status to ACTIVE", async () => {
      vi.spyOn(prismaService.federationConnection, "findFirst").mockResolvedValue(mockConnection);
      vi.spyOn(prismaService.federationConnection, "update").mockReturnValue({
        ...mockConnection,
        status: FederationConnectionStatus.ACTIVE,
        connectedAt: new Date(),
      });

      const result = await service.acceptConnection(mockWorkspaceId, mockConnection.id);

      expect(result.status).toBe(FederationConnectionStatus.ACTIVE);
      expect(result.connectedAt).toBeDefined();
      expect(prismaService.federationConnection.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            id: mockConnection.id,
          }),
          data: expect.objectContaining({
            status: FederationConnectionStatus.ACTIVE,
            connectedAt: expect.any(Date),
          }),
        })
      );
    });

    it("should throw error if connection not found", async () => {
      vi.spyOn(prismaService.federationConnection, "findFirst").mockResolvedValue(null);

      await expect(service.acceptConnection(mockWorkspaceId, "non-existent-id")).rejects.toThrow(
        "Connection not found"
      );
    });

    it("should enforce workspace isolation", async () => {
      vi.spyOn(prismaService.federationConnection, "findFirst").mockResolvedValue(null);

      await expect(
        service.acceptConnection("different-workspace", mockConnection.id)
      ).rejects.toThrow("Connection not found");
    });
  });

  describe("rejectConnection", () => {
    it("should update connection status to DISCONNECTED", async () => {
      vi.spyOn(prismaService.federationConnection, "findFirst").mockResolvedValue(mockConnection);
      vi.spyOn(prismaService.federationConnection, "update").mockReturnValue({
        ...mockConnection,
        status: FederationConnectionStatus.DISCONNECTED,
        metadata: { rejectionReason: "Not approved" },
      });

      const result = await service.rejectConnection(
        mockWorkspaceId,
        mockConnection.id,
        "Not approved"
      );

      expect(result.status).toBe(FederationConnectionStatus.DISCONNECTED);
      expect(result.metadata).toHaveProperty("rejectionReason", "Not approved");
    });

    it("should throw error if connection not found", async () => {
      vi.spyOn(prismaService.federationConnection, "findFirst").mockResolvedValue(null);

      await expect(
        service.rejectConnection(mockWorkspaceId, "non-existent-id", "Reason")
      ).rejects.toThrow("Connection not found");
    });
  });

  describe("disconnect", () => {
    const activeConnection: FederationConnection = {
      ...mockConnection,
      status: FederationConnectionStatus.ACTIVE,
      connectedAt: new Date(),
    };

    it("should disconnect active connection", async () => {
      vi.spyOn(prismaService.federationConnection, "findFirst").mockResolvedValue(activeConnection);
      vi.spyOn(prismaService.federationConnection, "update").mockReturnValue({
        ...activeConnection,
        status: FederationConnectionStatus.DISCONNECTED,
        disconnectedAt: new Date(),
      });

      const result = await service.disconnect(
        mockWorkspaceId,
        mockConnection.id,
        "Manual disconnect"
      );

      expect(result.status).toBe(FederationConnectionStatus.DISCONNECTED);
      expect(result.disconnectedAt).toBeDefined();
    });

    it("should store disconnection reason in metadata", async () => {
      vi.spyOn(prismaService.federationConnection, "findFirst").mockResolvedValue(activeConnection);
      vi.spyOn(prismaService.federationConnection, "update").mockReturnValue({
        ...activeConnection,
        status: FederationConnectionStatus.DISCONNECTED,
        disconnectedAt: new Date(),
        metadata: { disconnectReason: "Test reason" },
      });

      const result = await service.disconnect(mockWorkspaceId, mockConnection.id, "Test reason");

      expect(result.metadata).toHaveProperty("disconnectReason", "Test reason");
    });
  });

  describe("getConnections", () => {
    it("should list all connections for workspace", async () => {
      const connections = [mockConnection];
      vi.spyOn(prismaService.federationConnection, "findMany").mockResolvedValue(connections);

      const result = await service.getConnections(mockWorkspaceId);

      expect(result).toEqual(connections);
      expect(prismaService.federationConnection.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { workspaceId: mockWorkspaceId },
        })
      );
    });

    it("should filter by status if provided", async () => {
      const connections = [mockConnection];
      vi.spyOn(prismaService.federationConnection, "findMany").mockResolvedValue(connections);

      await service.getConnections(mockWorkspaceId, FederationConnectionStatus.ACTIVE);

      expect(prismaService.federationConnection.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: {
            workspaceId: mockWorkspaceId,
            status: FederationConnectionStatus.ACTIVE,
          },
        })
      );
    });
  });

  describe("getConnection", () => {
    it("should return connection details", async () => {
      vi.spyOn(prismaService.federationConnection, "findFirst").mockResolvedValue(mockConnection);

      const result = await service.getConnection(mockWorkspaceId, mockConnection.id);

      expect(result).toEqual(mockConnection);
    });

    it("should throw error if connection not found", async () => {
      vi.spyOn(prismaService.federationConnection, "findFirst").mockResolvedValue(null);

      await expect(service.getConnection(mockWorkspaceId, "non-existent-id")).rejects.toThrow(
        "Connection not found"
      );
    });

    it("should enforce workspace isolation", async () => {
      vi.spyOn(prismaService.federationConnection, "findFirst").mockResolvedValue(null);

      await expect(service.getConnection("different-workspace", mockConnection.id)).rejects.toThrow(
        "Connection not found"
      );
    });
  });

  describe("handleIncomingConnectionRequest", () => {
    const mockRequest = {
      instanceId: mockRemoteIdentity.instanceId,
      instanceUrl: mockRemoteIdentity.url,
      publicKey: mockRemoteIdentity.publicKey,
      capabilities: mockRemoteIdentity.capabilities,
      timestamp: Date.now(),
      signature: "valid-signature",
    };

    it("should reject request with incompatible protocol version", async () => {
      const incompatibleRequest = {
        ...mockRequest,
        capabilities: {
          ...mockRemoteIdentity.capabilities,
          protocolVersion: "2.0",
        },
      };

      vi.spyOn(signatureService, "verifyConnectionRequest").mockResolvedValue({ valid: true });

      await expect(
        service.handleIncomingConnectionRequest(mockWorkspaceId, incompatibleRequest)
      ).rejects.toThrow("Incompatible protocol version. Expected 1.0, received 2.0");
    });

    it("should accept request with compatible protocol version", async () => {
      const compatibleRequest = {
        ...mockRequest,
        capabilities: {
          ...mockRemoteIdentity.capabilities,
          protocolVersion: "1.0",
        },
      };

      vi.spyOn(signatureService, "verifyConnectionRequest").mockResolvedValue({ valid: true });
      vi.spyOn(prismaService.federationConnection, "create").mockResolvedValue(mockConnection);

      const result = await service.handleIncomingConnectionRequest(
        mockWorkspaceId,
        compatibleRequest
      );

      expect(result.status).toBe(FederationConnectionStatus.PENDING);
    });

    it("should validate connection request signature", async () => {
      const verifySpy = vi.spyOn(signatureService, "verifyConnectionRequest");
      vi.spyOn(prismaService.federationConnection, "create").mockResolvedValue(mockConnection);

      await service.handleIncomingConnectionRequest(mockWorkspaceId, mockRequest);

      expect(verifySpy).toHaveBeenCalledWith(mockRequest);
    });

    it("should create pending connection for valid request", async () => {
      vi.spyOn(signatureService, "verifyConnectionRequest").mockResolvedValue({ valid: true });
      vi.spyOn(prismaService.federationConnection, "create").mockResolvedValue(mockConnection);

      const result = await service.handleIncomingConnectionRequest(mockWorkspaceId, mockRequest);

      expect(result.status).toBe(FederationConnectionStatus.PENDING);
      expect(prismaService.federationConnection.create).toHaveBeenCalled();
    });

    it("should reject request with invalid signature", async () => {
      vi.spyOn(signatureService, "verifyConnectionRequest").mockResolvedValue({
        valid: false,
        error: "Invalid signature",
      });

      await expect(
        service.handleIncomingConnectionRequest(mockWorkspaceId, mockRequest)
      ).rejects.toThrow("Invalid connection request signature");
    });

    it("should log incoming connection attempt", async () => {
      vi.spyOn(signatureService, "verifyConnectionRequest").mockResolvedValue({ valid: true });
      vi.spyOn(prismaService.federationConnection, "create").mockResolvedValue(mockConnection);
      const auditSpy = vi.spyOn(auditService, "logIncomingConnectionAttempt");

      await service.handleIncomingConnectionRequest(mockWorkspaceId, mockRequest);

      expect(auditSpy).toHaveBeenCalledWith({
        workspaceId: mockWorkspaceId,
        remoteInstanceId: mockRequest.instanceId,
        remoteUrl: mockRequest.instanceUrl,
        timestamp: mockRequest.timestamp,
      });
    });

    it("should log connection created on success", async () => {
      vi.spyOn(signatureService, "verifyConnectionRequest").mockResolvedValue({ valid: true });
      vi.spyOn(prismaService.federationConnection, "create").mockResolvedValue(mockConnection);
      const auditSpy = vi.spyOn(auditService, "logIncomingConnectionCreated");

      await service.handleIncomingConnectionRequest(mockWorkspaceId, mockRequest);

      expect(auditSpy).toHaveBeenCalledWith({
        workspaceId: mockWorkspaceId,
        connectionId: mockConnection.id,
        remoteInstanceId: mockRequest.instanceId,
        remoteUrl: mockRequest.instanceUrl,
      });
    });

    it("should log connection rejected on invalid signature", async () => {
      vi.spyOn(signatureService, "verifyConnectionRequest").mockResolvedValue({
        valid: false,
        error: "Invalid signature",
      });
      const auditSpy = vi.spyOn(auditService, "logIncomingConnectionRejected");

      await expect(
        service.handleIncomingConnectionRequest(mockWorkspaceId, mockRequest)
      ).rejects.toThrow();

      expect(auditSpy).toHaveBeenCalledWith({
        workspaceId: mockWorkspaceId,
        remoteInstanceId: mockRequest.instanceId,
        remoteUrl: mockRequest.instanceUrl,
        reason: "Invalid signature",
        error: "Invalid signature",
      });
    });
  });
});
