import 'reflect-metadata';

import { Reflector } from '@nestjs/core';
import { Test, TestingModule } from '@nestjs/testing';
import request from 'supertest';
import { vi } from 'vitest';
import { PaymentsController } from './PaymentsController';
import { PaymentsService } from './PaymentsService';
import { AuthenticationGuard } from '../auth/AuthenticationGuard';
import { AuthorizationGuard } from '../auth/AuthorizationGuard';
import type { AuthorizationService } from '../auth/AuthorizationService';
import { FeatureAvailabilityGuard } from '../policy/FeatureAvailabilityGuard';
import type { PolicyService, PolicySnapshot } from '../policy/PolicyService';
import {
  GetPaymentsQuery,
  CreatePaymentInput,
  UpdatePaymentStatusInput,
  GetMembershipFeesQuery,
  UpsertMembershipFeeInput,
  UpdateMemberPaymentInput,
  GetPrizesQuery,
  CreatePrizeInput,
  AwardPrizeInput,
  DeletePrizeInput,
  GetWalletQuery,
  ProcessWalletTransactionInput,
  PaymentType,
  PayerType,
  PaymentStatus,
  MembershipFeeType,
  MemberPaymentStatus,
  PrizeType,
  TransactionType,
  GetPaymentsOutput,
  CreatePaymentOutput,
  UpdatePaymentStatusOutput,
  GetMembershipFeesOutput,
  UpsertMembershipFeeOutput,
  UpdateMemberPaymentOutput,
  GetPrizesOutput,
  CreatePrizeOutput,
  AwardPrizeOutput,
  DeletePrizeOutput,
  GetWalletOutput,
  ProcessWalletTransactionOutput
} from './dtos/PaymentsDto';

describe('PaymentsController', () => {
  let controller: PaymentsController;
  let service: ReturnType<typeof vi.mocked<PaymentsService>>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [PaymentsController],
      providers: [
        {
          provide: PaymentsService,
          useValue: {
            getPayments: vi.fn(),
            createPayment: vi.fn(),
            updatePaymentStatus: vi.fn(),
            getMembershipFees: vi.fn(),
            upsertMembershipFee: vi.fn(),
            updateMemberPayment: vi.fn(),
            getPrizes: vi.fn(),
            createPrize: vi.fn(),
            awardPrize: vi.fn(),
            deletePrize: vi.fn(),
            getWallet: vi.fn(),
            processWalletTransaction: vi.fn(),
          },
        },
      ],
    }).compile();

    controller = module.get<PaymentsController>(PaymentsController);
    service = vi.mocked(module.get(PaymentsService));
  });

  describe('getPayments', () => {
    it('should return payments', async () => {
      const query: GetPaymentsQuery = { leagueId: 'league-123' };
      const result: GetPaymentsOutput = { payments: [] };
      service.getPayments.mockResolvedValue(result);

      const response = await controller.getPayments(query);

      expect(service.getPayments).toHaveBeenCalledWith(query);
      expect(response).toEqual(result);
    });
  });

  describe('createPayment', () => {
    it('should create payment', async () => {
      const input: CreatePaymentInput = {
        amount: 100,
        type: PaymentType.MEMBERSHIP_FEE,
        payerId: 'payer-123',
        payerType: PayerType.DRIVER,
        leagueId: 'league-123'
      };
      const result: CreatePaymentOutput = {
        payment: {
          id: 'pay-123',
          type: PaymentType.MEMBERSHIP_FEE,
          amount: 100,
          platformFee: 5,
          netAmount: 95,
          payerId: 'payer-123',
          payerType: PayerType.DRIVER,
          leagueId: 'league-123',
          status: PaymentStatus.PENDING,
          createdAt: new Date()
        }
      };
      service.createPayment.mockResolvedValue(result);

      const response = await controller.createPayment(input);

      expect(service.createPayment).toHaveBeenCalledWith(input);
      expect(response).toEqual(result);
    });
  });

  describe('updatePaymentStatus', () => {
    it('should update payment status', async () => {
      const input: UpdatePaymentStatusInput = { paymentId: 'pay-123', status: PaymentStatus.COMPLETED };
      const result: UpdatePaymentStatusOutput = {
        payment: {
          id: 'pay-123',
          type: PaymentType.MEMBERSHIP_FEE,
          amount: 100,
          platformFee: 5,
          netAmount: 95,
          payerId: 'payer-123',
          payerType: PayerType.DRIVER,
          leagueId: 'league-123',
          status: PaymentStatus.COMPLETED,
          createdAt: new Date(),
          completedAt: new Date()
        }
      };
      service.updatePaymentStatus.mockResolvedValue(result);

      const response = await controller.updatePaymentStatus(input);

      expect(service.updatePaymentStatus).toHaveBeenCalledWith(input);
      expect(response).toEqual(result);
    });
  });

  describe('getMembershipFees', () => {
    it('should return membership fees', async () => {
      const query: GetMembershipFeesQuery = { leagueId: 'league-123' };
      const result: GetMembershipFeesOutput = {
        fee: {
          id: 'fee-123',
          leagueId: 'league-123',
          type: MembershipFeeType.MONTHLY,
          amount: 50,
          enabled: true,
          createdAt: new Date(),
          updatedAt: new Date()
        },
        payments: []
      };
      service.getMembershipFees.mockResolvedValue(result);

      const response = await controller.getMembershipFees(query);

      expect(service.getMembershipFees).toHaveBeenCalledWith(query);
      expect(response).toEqual(result);
    });
  });

  describe('upsertMembershipFee', () => {
    it('should upsert membership fee', async () => {
      const input: UpsertMembershipFeeInput = {
        leagueId: 'league-123',
        type: MembershipFeeType.MONTHLY,
        amount: 50
      };
      const result: UpsertMembershipFeeOutput = {
        fee: {
          id: 'fee-123',
          leagueId: 'league-123',
          type: MembershipFeeType.MONTHLY,
          amount: 50,
          enabled: true,
          createdAt: new Date(),
          updatedAt: new Date()
        }
      };
      service.upsertMembershipFee.mockResolvedValue(result);

      const response = await controller.upsertMembershipFee(input);

      expect(service.upsertMembershipFee).toHaveBeenCalledWith(input);
      expect(response).toEqual(result);
    });
  });

  describe('updateMemberPayment', () => {
    it('should update member payment', async () => {
      const input: UpdateMemberPaymentInput = {
        feeId: 'fee-123',
        driverId: 'driver-123',
        status: MemberPaymentStatus.PAID
      };
      const result: UpdateMemberPaymentOutput = {
        payment: {
          id: 'mp-123',
          feeId: 'fee-123',
          driverId: 'driver-123',
          amount: 50,
          platformFee: 2.5,
          netAmount: 47.5,
          status: MemberPaymentStatus.PAID,
          dueDate: new Date(),
          paidAt: new Date()
        }
      };
      service.updateMemberPayment.mockResolvedValue(result);

      const response = await controller.updateMemberPayment(input);

      expect(service.updateMemberPayment).toHaveBeenCalledWith(input);
      expect(response).toEqual(result);
    });
  });

  describe('getPrizes', () => {
    it('should return prizes', async () => {
      const query: GetPrizesQuery = { leagueId: 'league-123' };
      const result: GetPrizesOutput = { prizes: [] };
      service.getPrizes.mockResolvedValue(result);

      const response = await controller.getPrizes(query);

      expect(service.getPrizes).toHaveBeenCalledWith(query);
      expect(response).toEqual(result);
    });
  });

  describe('createPrize', () => {
    it('should create prize', async () => {
      const input: CreatePrizeInput = {
        leagueId: 'league-123',
        seasonId: 'season-123',
        position: 1,
        name: 'Champion',
        amount: 1000,
        type: PrizeType.CASH
      };
      const result: CreatePrizeOutput = {
        prize: {
          id: 'prize-123',
          leagueId: 'league-123',
          seasonId: 'season-123',
          position: 1,
          name: 'Champion',
          amount: 1000,
          type: PrizeType.CASH,
          awarded: false,
          createdAt: new Date()
        }
      };
      service.createPrize.mockResolvedValue(result);

      const response = await controller.createPrize(input);

      expect(service.createPrize).toHaveBeenCalledWith(input);
      expect(response).toEqual(result);
    });
  });

  describe('awardPrize', () => {
    it('should award prize', async () => {
      const input: AwardPrizeInput = { prizeId: 'prize-123', driverId: 'driver-123' };
      const result: AwardPrizeOutput = {
        prize: {
          id: 'prize-123',
          leagueId: 'league-123',
          seasonId: 'season-123',
          position: 1,
          name: 'Champion',
          amount: 1000,
          type: PrizeType.CASH,
          awarded: true,
          awardedTo: 'driver-123',
          awardedAt: new Date(),
          createdAt: new Date()
        }
      };
      service.awardPrize.mockResolvedValue(result);

      const response = await controller.awardPrize(input);

      expect(service.awardPrize).toHaveBeenCalledWith(input);
      expect(response).toEqual(result);
    });
  });

  describe('deletePrize', () => {
    it('should delete prize', async () => {
      const query: DeletePrizeInput = { prizeId: 'prize-123' };
      const result: DeletePrizeOutput = { success: true };
      service.deletePrize.mockResolvedValue(result);

      const response = await controller.deletePrize(query);

      expect(service.deletePrize).toHaveBeenCalledWith(query);
      expect(response).toEqual(result);
    });
  });

  describe('getWallet', () => {
    it('should return wallet', async () => {
      const query: GetWalletQuery = { leagueId: 'league-123' };
      const result: GetWalletOutput = {
        wallet: {
          id: 'wallet-123',
          leagueId: 'league-123',
          balance: 100,
          totalRevenue: 100,
          totalPlatformFees: 5,
          totalWithdrawn: 0,
          currency: 'USD',
          createdAt: new Date()
        },
        transactions: []
      };
      service.getWallet.mockResolvedValue(result);

      const response = await controller.getWallet(query);

      expect(service.getWallet).toHaveBeenCalledWith(query);
      expect(response).toEqual(result);
    });
  });

  describe('processWalletTransaction', () => {
    it('should process wallet transaction', async () => {
      const input: ProcessWalletTransactionInput = {
        leagueId: 'league-123',
        amount: 50,
        type: TransactionType.DEPOSIT,
        description: 'Test deposit'
      };
      const result: ProcessWalletTransactionOutput = {
        wallet: {
          id: 'wallet-123',
          leagueId: 'league-123',
          balance: 150,
          totalRevenue: 150,
          totalPlatformFees: 7.5,
          totalWithdrawn: 0,
          currency: 'USD',
          createdAt: new Date()
        },
        transaction: {
          id: 'tx-123',
          walletId: 'wallet-123',
          type: TransactionType.DEPOSIT,
          amount: 50,
          description: 'Test deposit',
          createdAt: new Date()
        }
      };
      service.processWalletTransaction.mockResolvedValue(result);

      const response = await controller.processWalletTransaction(input);

      expect(service.processWalletTransaction).toHaveBeenCalledWith(input);
      expect(response).toEqual(result);
    });
  });

  describe('auth guards (HTTP)', () => {
    let app: any;

    const sessionPort: { getCurrentSession: () => Promise<null | { token: string; user: { id: string } }> } = {
      getCurrentSession: vi.fn(async () => null),
    };

    const authorizationService: Pick<AuthorizationService, 'getRolesForUser'> = {
      getRolesForUser: vi.fn(() => []),
    };

    const policyService: Pick<PolicyService, 'getSnapshot'> = {
      getSnapshot: vi.fn(async (): Promise<PolicySnapshot> => ({
        policyVersion: 1,
        operationalMode: 'normal',
        maintenanceAllowlist: { view: [], mutate: [] },
        capabilities: { 'payments.admin': 'enabled' },
        loadedFrom: 'defaults',
        loadedAtIso: new Date(0).toISOString(),
      })),
    };

    beforeEach(async () => {
      const module = await Test.createTestingModule({
        controllers: [PaymentsController],
        providers: [
          {
            provide: PaymentsService,
            useValue: {
              getPayments: vi.fn(async () => ({ payments: [] })),
            },
          },
        ],
      }).compile();

      app = module.createNestApplication();

      const reflector = new Reflector();
      app.useGlobalGuards(
        new AuthenticationGuard(sessionPort as any),
        new AuthorizationGuard(reflector, authorizationService as any),
        new FeatureAvailabilityGuard(reflector, policyService as any),
      );

      await app.init();
    });

    afterEach(async () => {
      await app?.close();
      vi.clearAllMocks();
    });

    it('denies endpoint when not authenticated (401)', async () => {
      await request(app.getHttpServer()).get('/payments').expect(401);
    });

    it('returns 403 when authenticated but missing required role', async () => {
      vi.mocked(sessionPort.getCurrentSession).mockResolvedValueOnce({
        token: 't',
        user: { id: 'user-1' },
      });
      vi.mocked(authorizationService.getRolesForUser).mockReturnValueOnce(['user']);

      await request(app.getHttpServer()).get('/payments').expect(403);
    });

    it('returns 404 when role is satisfied but capability is disabled', async () => {
      vi.mocked(sessionPort.getCurrentSession).mockResolvedValueOnce({
        token: 't',
        user: { id: 'user-1' },
      });
      vi.mocked(authorizationService.getRolesForUser).mockReturnValueOnce(['admin']);
      vi.mocked(policyService.getSnapshot).mockResolvedValueOnce({
        policyVersion: 1,
        operationalMode: 'normal',
        maintenanceAllowlist: { view: [], mutate: [] },
        capabilities: { 'payments.admin': 'disabled' },
        loadedFrom: 'defaults',
        loadedAtIso: new Date(0).toISOString(),
      });

      await request(app.getHttpServer()).get('/payments').expect(404);
    });

    it('allows access when role is satisfied and capability is enabled', async () => {
      vi.mocked(sessionPort.getCurrentSession).mockResolvedValueOnce({
        token: 't',
        user: { id: 'user-1' },
      });
      vi.mocked(authorizationService.getRolesForUser).mockReturnValueOnce(['admin']);
      vi.mocked(policyService.getSnapshot).mockResolvedValueOnce({
        policyVersion: 1,
        operationalMode: 'normal',
        maintenanceAllowlist: { view: [], mutate: [] },
        capabilities: { 'payments.admin': 'enabled' },
        loadedFrom: 'defaults',
        loadedAtIso: new Date(0).toISOString(),
      });

      await request(app.getHttpServer()).get('/payments').expect(200);
    });
  });
});
