/**
 * MPC Service End-to-End Tests
 *
 * These tests simulate the full flow of MPC operations from API to database,
 * using mocked external services (MPC Coordinator, Message Router, TSS Library).
 */

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../../src/app.module';
import { PrismaService } from '../../src/infrastructure/persistence/prisma/prisma.service';
import { JwtService } from '@nestjs/jwt';
import { MPCCoordinatorClient } from '../../src/infrastructure/external/mpc-system/coordinator-client';
import { MPCMessageRouterClient } from '../../src/infrastructure/external/mpc-system/message-router-client';
import { TSS_PROTOCOL_SERVICE } from '../../src/domain/services/tss-protocol.domain-service';
import { EventPublisherService } from '../../src/infrastructure/messaging/kafka/event-publisher.service';
import { SessionCacheService } from '../../src/infrastructure/redis/cache/session-cache.service';
import { DistributedLockService } from '../../src/infrastructure/redis/lock/distributed-lock.service';
import { PartyShareType } from '../../src/domain/enums';

describe('MPC Service E2E Tests', () => {
  let app: INestApplication;
  let prismaService: any;
  let jwtService: JwtService;
  let authToken: string;

  // Mock services
  let mockCoordinatorClient: any;
  let mockMessageRouterClient: any;
  let mockTssProtocolService: any;
  let mockEventPublisher: any;
  let mockSessionCache: any;
  let mockDistributedLock: any;

  beforeAll(async () => {
    // Setup mock implementations
    mockCoordinatorClient = {
      joinSession: jest.fn(),
      reportCompletion: jest.fn(),
      reportRoundComplete: jest.fn(),
      reportSessionComplete: jest.fn(),
      reportSessionFailed: jest.fn(),
    };

    mockMessageRouterClient = {
      connect: jest.fn(),
      disconnect: jest.fn(),
      sendMessage: jest.fn(),
      subscribeMessages: jest.fn().mockResolvedValue({
        next: jest.fn().mockResolvedValue({ done: true, value: undefined }),
      }),
      onMessage: jest.fn(),
      waitForMessages: jest.fn(),
    };

    mockTssProtocolService = {
      runKeygen: jest.fn(),
      runSigning: jest.fn(),
      runRefresh: jest.fn(),
      initializeKeygen: jest.fn(),
      processKeygenRound: jest.fn(),
      finalizeKeygen: jest.fn(),
      initializeSigning: jest.fn(),
      processSigningRound: jest.fn(),
      finalizeSigning: jest.fn(),
      initializeRefresh: jest.fn(),
      processRefreshRound: jest.fn(),
      finalizeRefresh: jest.fn(),
    };

    mockEventPublisher = {
      publish: jest.fn(),
      publishAll: jest.fn(),
      publishBatch: jest.fn(),
      onModuleInit: jest.fn(),
      onModuleDestroy: jest.fn(),
    };

    mockSessionCache = {
      setSessionState: jest.fn(),
      getSessionState: jest.fn(),
      deleteSessionState: jest.fn(),
      setTssLocalState: jest.fn(),
      getTssLocalState: jest.fn(),
    };

    mockDistributedLock = {
      acquireLock: jest.fn().mockResolvedValue(true),
      releaseLock: jest.fn().mockResolvedValue(true),
      withLock: jest.fn().mockImplementation(async (_key: string, fn: () => Promise<any>) => fn()),
    };

    // Setup mock Prisma
    prismaService = {
      partyShare: {
        findUnique: jest.fn(),
        findFirst: jest.fn(),
        findMany: jest.fn(),
        create: jest.fn(),
        update: jest.fn(),
        delete: jest.fn(),
        count: jest.fn(),
      },
      sessionState: {
        findUnique: jest.fn(),
        findFirst: jest.fn(),
        findMany: jest.fn(),
        create: jest.fn(),
        update: jest.fn(),
      },
      $connect: jest.fn(),
      $disconnect: jest.fn(),
      $transaction: jest.fn((fn) => fn(prismaService)),
      cleanDatabase: jest.fn(),
    };

    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    })
      .overrideProvider(PrismaService)
      .useValue(prismaService)
      .overrideProvider(MPCCoordinatorClient)
      .useValue(mockCoordinatorClient)
      .overrideProvider(MPCMessageRouterClient)
      .useValue(mockMessageRouterClient)
      .overrideProvider(TSS_PROTOCOL_SERVICE)
      .useValue(mockTssProtocolService)
      .overrideProvider(EventPublisherService)
      .useValue(mockEventPublisher)
      .overrideProvider(SessionCacheService)
      .useValue(mockSessionCache)
      .overrideProvider(DistributedLockService)
      .useValue(mockDistributedLock)
      .compile();

    app = moduleFixture.createNestApplication();

    // Set global prefix to match production setup
    app.setGlobalPrefix('api/v1');

    app.useGlobalPipes(
      new ValidationPipe({
        whitelist: true,
        transform: true,
        forbidNonWhitelisted: true,
      }),
    );
    await app.init();

    jwtService = moduleFixture.get<JwtService>(JwtService);

    // Generate test auth token with correct payload structure
    authToken = jwtService.sign({
      sub: 'test-user-id',
      type: 'access',
      partyId: 'user123-server',
    });
  });

  afterAll(async () => {
    await app.close();
  });

  beforeEach(() => {
    jest.clearAllMocks();
  });

  describe('Health Check', () => {
    it('GET /api/v1/mpc-party/health - should return healthy status', async () => {
      const response = await request(app.getHttpServer())
        .get('/api/v1/mpc-party/health')
        .expect(200);

      // The response might be wrapped in { success, data } or might be raw based on interceptor
      const body = response.body.data || response.body;
      expect(body.status).toBe('ok');
      expect(body.service).toBe('mpc-party-service');
      expect(body.timestamp).toBeDefined();
    });
  });

  describe('Keygen Flow', () => {
    const keygenSessionId = '550e8400-e29b-41d4-a716-446655440000';

    it('POST /api/v1/mpc-party/keygen/participate - should accept keygen participation', async () => {
      // Async endpoint returns 202 immediately
      const response = await request(app.getHttpServer())
        .post('/api/v1/mpc-party/keygen/participate')
        .set('Authorization', `Bearer ${authToken}`)
        .send({
          sessionId: keygenSessionId,
          partyId: 'user123-server',
          joinToken: 'join-token-abc',
          shareType: PartyShareType.WALLET,
          userId: 'test-user-id',
        })
        .expect(202);

      // Response might be wrapped in { success, data }
      const body = response.body.data || response.body;
      expect(body.message).toBe('Keygen participation started');
      expect(body.sessionId).toBe(keygenSessionId);
      expect(body.partyId).toBe('user123-server');
    });

    it('POST /api/v1/mpc-party/keygen/participate - should validate required fields', async () => {
      await request(app.getHttpServer())
        .post('/api/v1/mpc-party/keygen/participate')
        .set('Authorization', `Bearer ${authToken}`)
        .send({
          // Missing required fields
          sessionId: keygenSessionId,
        })
        .expect(400);
    });
  });

  describe('Signing Flow', () => {
    const signingSessionId = '660e8400-e29b-41d4-a716-446655440001';

    it('POST /api/v1/mpc-party/signing/participate - should accept signing participation', async () => {
      const response = await request(app.getHttpServer())
        .post('/api/v1/mpc-party/signing/participate')
        .set('Authorization', `Bearer ${authToken}`)
        .send({
          sessionId: signingSessionId,
          partyId: 'user123-server',
          joinToken: 'join-token-abc',
          messageHash: 'a'.repeat(64),
          publicKey: '03' + '0'.repeat(64),
        })
        .expect(202);

      // Response might be wrapped in { success, data }
      const body = response.body.data || response.body;
      expect(body.message).toBe('Signing participation started');
      expect(body.sessionId).toBe(signingSessionId);
      expect(body.partyId).toBe('user123-server');
    });

    it('POST /api/v1/mpc-party/signing/participate - should validate message hash format', async () => {
      await request(app.getHttpServer())
        .post('/api/v1/mpc-party/signing/participate')
        .set('Authorization', `Bearer ${authToken}`)
        .send({
          sessionId: signingSessionId,
          partyId: 'user123-server',
          joinToken: 'join-token-abc',
          messageHash: 'invalid-not-64-hex-chars',
          publicKey: '03' + '0'.repeat(64),
        })
        .expect(400);
    });
  });

  describe('Share Rotation Flow', () => {
    const rotateSessionId = '770e8400-e29b-41d4-a716-446655440002';

    it('POST /api/v1/mpc-party/share/rotate - should accept rotation request', async () => {
      const response = await request(app.getHttpServer())
        .post('/api/v1/mpc-party/share/rotate')
        .set('Authorization', `Bearer ${authToken}`)
        .send({
          sessionId: rotateSessionId,
          partyId: 'user123-server',
          joinToken: 'join-token-abc',
          publicKey: '03' + '0'.repeat(64),
        })
        .expect(202);

      // Response can be wrapped or raw depending on interceptor
      const body = response.body.data || response.body;
      expect(body.message).toBe('Share rotation started');
      expect(body.sessionId).toBe(rotateSessionId);
      expect(body.partyId).toBe('user123-server');
    });
  });

  describe('Share Management', () => {
    const testShareId = 'share_1699887766123_abc123xyz';

    beforeEach(() => {
      const mockShareRecord = {
        id: testShareId,
        partyId: 'user123-server',
        sessionId: '550e8400-e29b-41d4-a716-446655440000',
        shareType: 'wallet',
        shareData: JSON.stringify({
          data: Buffer.from('encrypted-share-data').toString('base64'),
          iv: Buffer.from('123456789012').toString('base64'),
          authTag: Buffer.from('1234567890123456').toString('base64'),
        }),
        publicKey: '03' + '0'.repeat(64),
        thresholdT: 2,
        thresholdN: 3,
        status: 'active',
        createdAt: new Date('2024-01-01T00:00:00Z'),
        updatedAt: new Date('2024-01-01T00:00:00Z'),
        lastUsedAt: null,
      };

      prismaService.partyShare.findUnique.mockResolvedValue(mockShareRecord);
      prismaService.partyShare.findMany.mockResolvedValue([mockShareRecord]);
      prismaService.partyShare.count.mockResolvedValue(1);
    });

    it('GET /api/v1/mpc-party/shares - should list shares', async () => {
      const response = await request(app.getHttpServer())
        .get('/api/v1/mpc-party/shares')
        .set('Authorization', `Bearer ${authToken}`)
        .query({ page: 1, limit: 10 })
        .expect(200);

      // Response is wrapped in { success, data }
      const body = response.body.data || response.body;
      expect(body).toHaveProperty('items');
      expect(body).toHaveProperty('total');
      expect(body.items).toBeInstanceOf(Array);
    });

    it('GET /api/v1/mpc-party/shares/:shareId - should get share info', async () => {
      const response = await request(app.getHttpServer())
        .get(`/api/v1/mpc-party/shares/${testShareId}`)
        .set('Authorization', `Bearer ${authToken}`)
        .expect(200);

      // Response is wrapped in { success, data }
      const body = response.body.data || response.body;
      expect(body).toHaveProperty('id');
      expect(body).toHaveProperty('status');
    });

    it('GET /api/v1/mpc-party/shares/:shareId - should return error for invalid share id format', async () => {
      prismaService.partyShare.findUnique.mockResolvedValue(null);

      // Invalid shareId format will return 500 (validation fails in domain layer)
      await request(app.getHttpServer())
        .get('/api/v1/mpc-party/shares/non-existent-share')
        .set('Authorization', `Bearer ${authToken}`)
        .expect(500);
    });
  });

  describe('Authentication', () => {
    it('should reject requests without token', async () => {
      await request(app.getHttpServer())
        .get('/api/v1/mpc-party/shares')
        .expect(401);
    });

    it('should reject requests with invalid token', async () => {
      await request(app.getHttpServer())
        .get('/api/v1/mpc-party/shares')
        .set('Authorization', 'Bearer invalid-token')
        .expect(401);
    });

    it('should reject requests with expired token', async () => {
      const expiredToken = jwtService.sign(
        { sub: 'test-user', type: 'access' },
        { expiresIn: '-1h' },
      );

      await request(app.getHttpServer())
        .get('/api/v1/mpc-party/shares')
        .set('Authorization', `Bearer ${expiredToken}`)
        .expect(401);
    });

    it('should allow public endpoints without token', async () => {
      const response = await request(app.getHttpServer())
        .get('/api/v1/mpc-party/health')
        .expect(200);

      // Response might be wrapped in { success, data }
      const body = response.body.data || response.body;
      expect(body.status).toBe('ok');
    });
  });

  describe('Error Handling', () => {
    it('should return structured error for validation failures', async () => {
      const response = await request(app.getHttpServer())
        .post('/api/v1/mpc-party/keygen/participate')
        .set('Authorization', `Bearer ${authToken}`)
        .send({
          // Missing required fields
        })
        .expect(400);

      expect(response.body).toHaveProperty('message');
    });

    it('should handle internal server errors gracefully', async () => {
      prismaService.partyShare.findMany.mockRejectedValue(
        new Error('Database connection lost'),
      );

      const response = await request(app.getHttpServer())
        .get('/api/v1/mpc-party/shares')
        .set('Authorization', `Bearer ${authToken}`)
        .expect(500);

      expect(response.body).toHaveProperty('message');
    });
  });
});
