/**
 * Investment Transaction Service Unit Tests
 *
 * Tests for transaction service including:
 * - Deposits and withdrawals
 * - Transaction tracking
 * - Distribution processing
 * - Fee calculations
 */

import { mockDb, createMockQueryResult, resetDatabaseMocks } from '../../../../__tests__/mocks/database.mock';

// Mock database
jest.mock('../../../../shared/database', () => ({
  db: mockDb,
}));

// Mock logger
jest.mock('../../../../shared/utils/logger', () => ({
  logger: {
    info: jest.fn(),
    error: jest.fn(),
    warn: jest.fn(),
    debug: jest.fn(),
  },
}));

// Mock account service
const mockGetAccountById = jest.fn();
const mockUpdateAccountBalance = jest.fn();
jest.mock('../account.service', () => ({
  accountService: {
    getAccountById: mockGetAccountById,
    updateAccountBalance: mockUpdateAccountBalance,
  },
}));

// Import service after mocks
import { transactionService } from '../transaction.service';

describe('TransactionService', () => {
  beforeEach(() => {
    resetDatabaseMocks();
    mockGetAccountById.mockReset();
    mockUpdateAccountBalance.mockReset();
    jest.clearAllMocks();
  });

  describe('createDeposit', () => {
    it('should create a new deposit transaction', async () => {
      const mockAccount = {
        id: 'account-123',
        userId: 'user-123',
        balance: 1000,
        status: 'active',
      };

      mockGetAccountById.mockResolvedValueOnce(mockAccount);
      mockUpdateAccountBalance.mockResolvedValueOnce({
        ...mockAccount,
        balance: 1500,
      });

      const result = await transactionService.createDeposit({
        accountId: 'account-123',
        amount: 500,
        stripePaymentId: 'pi_123456',
      });

      expect(result.type).toBe('deposit');
      expect(result.amount).toBe(500);
      expect(result.status).toBe('completed');
      expect(result.balanceBefore).toBe(1000);
      expect(result.balanceAfter).toBe(1500);
      expect(result.stripePaymentId).toBe('pi_123456');
    });

    it('should validate minimum deposit amount', async () => {
      await expect(
        transactionService.createDeposit({
          accountId: 'account-123',
          amount: 5,
        })
      ).rejects.toThrow('Minimum deposit amount is 10');
    });

    it('should reject deposit to suspended account', async () => {
      const mockAccount = {
        id: 'account-123',
        userId: 'user-123',
        balance: 1000,
        status: 'suspended',
      };

      mockGetAccountById.mockResolvedValueOnce(mockAccount);

      await expect(
        transactionService.createDeposit({
          accountId: 'account-123',
          amount: 500,
        })
      ).rejects.toThrow('Cannot deposit to suspended account');
    });

    it('should reject deposit to closed account', async () => {
      const mockAccount = {
        id: 'account-123',
        userId: 'user-123',
        balance: 0,
        status: 'closed',
      };

      mockGetAccountById.mockResolvedValueOnce(mockAccount);

      await expect(
        transactionService.createDeposit({
          accountId: 'account-123',
          amount: 500,
        })
      ).rejects.toThrow('Cannot deposit to closed account');
    });

    it('should handle account not found', async () => {
      mockGetAccountById.mockResolvedValueOnce(null);

      await expect(
        transactionService.createDeposit({
          accountId: 'non-existent',
          amount: 500,
        })
      ).rejects.toThrow('Account not found');
    });
  });

  describe('createWithdrawalRequest', () => {
    it('should create withdrawal request with bank info', async () => {
      const mockAccount = {
        id: 'account-123',
        userId: 'user-123',
        balance: 5000,
        status: 'active',
      };

      mockGetAccountById.mockResolvedValueOnce(mockAccount);

      const result = await transactionService.createWithdrawalRequest({
        accountId: 'account-123',
        amount: 1000,
        bankInfo: {
          bankName: 'Bank of Test',
          accountNumber: '1234567890',
          routingNumber: '987654321',
          accountHolderName: 'John Doe',
        },
      });

      expect(result.amount).toBe(1000);
      expect(result.status).toBe('pending');
      expect(result.bankInfo).toBeDefined();
      expect(result.bankInfo?.bankName).toBe('Bank of Test');
      expect(result.cryptoInfo).toBeNull();
    });

    it('should create withdrawal request with crypto info', async () => {
      const mockAccount = {
        id: 'account-123',
        userId: 'user-123',
        balance: 5000,
        status: 'active',
      };

      mockGetAccountById.mockResolvedValueOnce(mockAccount);

      const result = await transactionService.createWithdrawalRequest({
        accountId: 'account-123',
        amount: 2000,
        cryptoInfo: {
          network: 'ethereum',
          address: '0x1234567890abcdef',
        },
      });

      expect(result.cryptoInfo).toBeDefined();
      expect(result.cryptoInfo?.network).toBe('ethereum');
      expect(result.cryptoInfo?.address).toBe('0x1234567890abcdef');
      expect(result.bankInfo).toBeNull();
    });

    it('should validate minimum withdrawal amount', async () => {
      await expect(
        transactionService.createWithdrawalRequest({
          accountId: 'account-123',
          amount: 5,
        })
      ).rejects.toThrow('Minimum withdrawal amount is 50');
    });

    it('should validate sufficient balance', async () => {
      const mockAccount = {
        id: 'account-123',
        userId: 'user-123',
        balance: 100,
        status: 'active',
      };

      mockGetAccountById.mockResolvedValueOnce(mockAccount);

      await expect(
        transactionService.createWithdrawalRequest({
          accountId: 'account-123',
          amount: 500,
        })
      ).rejects.toThrow('Insufficient balance');
    });

    it('should require either bank or crypto info', async () => {
      const mockAccount = {
        id: 'account-123',
        userId: 'user-123',
        balance: 5000,
        status: 'active',
      };

      mockGetAccountById.mockResolvedValueOnce(mockAccount);

      await expect(
        transactionService.createWithdrawalRequest({
          accountId: 'account-123',
          amount: 1000,
        })
      ).rejects.toThrow('Either bank info or crypto info is required');
    });
  });

  describe('processWithdrawal', () => {
    it('should process approved withdrawal', async () => {
      const mockAccount = {
        id: 'account-123',
        userId: 'user-123',
        balance: 5000,
        status: 'active',
      };

      const mockWithdrawal = {
        id: 'withdrawal-123',
        accountId: 'account-123',
        amount: 1000,
        status: 'pending',
      };

      mockGetAccountById.mockResolvedValueOnce(mockAccount);
      mockUpdateAccountBalance.mockResolvedValueOnce({
        ...mockAccount,
        balance: 4000,
      });

      const result = await transactionService.processWithdrawal(
        'withdrawal-123',
        'approved'
      );

      expect(result.status).toBe('completed');
      expect(result.completedAt).toBeDefined();
      expect(mockUpdateAccountBalance).toHaveBeenCalledWith('account-123', 4000);
    });

    it('should reject withdrawal with reason', async () => {
      const mockWithdrawal = {
        id: 'withdrawal-123',
        accountId: 'account-123',
        amount: 1000,
        status: 'pending',
      };

      const result = await transactionService.processWithdrawal(
        'withdrawal-123',
        'rejected',
        'Suspicious activity detected'
      );

      expect(result.status).toBe('rejected');
      expect(result.rejectionReason).toBe('Suspicious activity detected');
      expect(mockUpdateAccountBalance).not.toHaveBeenCalled();
    });

    it('should handle withdrawal not found', async () => {
      await expect(
        transactionService.processWithdrawal('non-existent', 'approved')
      ).rejects.toThrow('Withdrawal request not found');
    });

    it('should prevent processing already completed withdrawal', async () => {
      const mockWithdrawal = {
        id: 'withdrawal-123',
        accountId: 'account-123',
        amount: 1000,
        status: 'completed',
      };

      await expect(
        transactionService.processWithdrawal('withdrawal-123', 'approved')
      ).rejects.toThrow('Withdrawal already processed');
    });
  });

  describe('getAccountTransactions', () => {
    it('should retrieve all transactions for an account', async () => {
      const mockTransactions = [
        {
          id: 'tx-1',
          accountId: 'account-123',
          type: 'deposit',
          amount: 1000,
          status: 'completed',
          createdAt: new Date(),
        },
        {
          id: 'tx-2',
          accountId: 'account-123',
          type: 'withdrawal',
          amount: 500,
          status: 'completed',
          createdAt: new Date(),
        },
      ];

      const result = await transactionService.getAccountTransactions('account-123');

      expect(result).toBeDefined();
      expect(Array.isArray(result)).toBe(true);
    });

    it('should filter transactions by type', async () => {
      const result = await transactionService.getAccountTransactions('account-123', {
        type: 'deposit',
      });

      expect(result.every(tx => tx.type === 'deposit')).toBe(true);
    });

    it('should filter transactions by status', async () => {
      const result = await transactionService.getAccountTransactions('account-123', {
        status: 'completed',
      });

      expect(result.every(tx => tx.status === 'completed')).toBe(true);
    });

    it('should filter transactions by date range', async () => {
      const startDate = new Date('2024-01-01');
      const endDate = new Date('2024-12-31');

      const result = await transactionService.getAccountTransactions('account-123', {
        startDate,
        endDate,
      });

      result.forEach(tx => {
        expect(tx.createdAt >= startDate).toBe(true);
        expect(tx.createdAt <= endDate).toBe(true);
      });
    });

    it('should limit results', async () => {
      const result = await transactionService.getAccountTransactions('account-123', {
        limit: 10,
      });

      expect(result.length).toBeLessThanOrEqual(10);
    });
  });

  describe('createDistribution', () => {
    it('should create earnings distribution', async () => {
      const mockAccount = {
        id: 'account-123',
        userId: 'user-123',
        balance: 10000,
        product: {
          performanceFee: 20,
        },
      };

      mockGetAccountById.mockResolvedValueOnce(mockAccount);

      const result = await transactionService.createDistribution({
        accountId: 'account-123',
        periodStart: new Date('2024-01-01'),
        periodEnd: new Date('2024-01-31'),
        grossEarnings: 1000,
      });

      expect(result.grossEarnings).toBe(1000);
      expect(result.performanceFee).toBe(200);
      expect(result.netEarnings).toBe(800);
      expect(result.status).toBe('pending');
    });

    it('should calculate performance fee correctly', async () => {
      const mockAccount = {
        id: 'account-123',
        userId: 'user-123',
        balance: 10000,
        product: {
          performanceFee: 25,
        },
      };

      mockGetAccountById.mockResolvedValueOnce(mockAccount);

      const result = await transactionService.createDistribution({
        accountId: 'account-123',
        periodStart: new Date('2024-01-01'),
        periodEnd: new Date('2024-01-31'),
        grossEarnings: 2000,
      });

      expect(result.performanceFee).toBe(500);
      expect(result.netEarnings).toBe(1500);
    });

    it('should handle zero or negative earnings', async () => {
      const mockAccount = {
        id: 'account-123',
        userId: 'user-123',
        balance: 10000,
        product: {
          performanceFee: 20,
        },
      };

      mockGetAccountById.mockResolvedValueOnce(mockAccount);

      const result = await transactionService.createDistribution({
        accountId: 'account-123',
        periodStart: new Date('2024-01-01'),
        periodEnd: new Date('2024-01-31'),
        grossEarnings: -500,
      });

      expect(result.grossEarnings).toBe(-500);
      expect(result.performanceFee).toBe(0);
      expect(result.netEarnings).toBe(-500);
    });
  });

  describe('processDistribution', () => {
    it('should process pending distribution', async () => {
      const mockAccount = {
        id: 'account-123',
        userId: 'user-123',
        balance: 10000,
        status: 'active',
      };

      const mockDistribution = {
        id: 'dist-123',
        accountId: 'account-123',
        netEarnings: 800,
        status: 'pending',
      };

      mockGetAccountById.mockResolvedValueOnce(mockAccount);
      mockUpdateAccountBalance.mockResolvedValueOnce({
        ...mockAccount,
        balance: 10800,
      });

      const result = await transactionService.processDistribution('dist-123');

      expect(result.status).toBe('distributed');
      expect(result.distributedAt).toBeDefined();
      expect(mockUpdateAccountBalance).toHaveBeenCalledWith('account-123', 10800);
    });

    it('should create transaction record for distribution', async () => {
      const mockAccount = {
        id: 'account-123',
        userId: 'user-123',
        balance: 10000,
        status: 'active',
      };

      mockGetAccountById.mockResolvedValueOnce(mockAccount);
      mockUpdateAccountBalance.mockResolvedValueOnce({
        ...mockAccount,
        balance: 10800,
      });

      await transactionService.processDistribution('dist-123');

      // Verify transaction was created
      const transactions = await transactionService.getAccountTransactions('account-123');
      const distributionTx = transactions.find(tx => tx.type === 'distribution');

      expect(distributionTx).toBeDefined();
    });

    it('should handle negative distribution (loss)', async () => {
      const mockAccount = {
        id: 'account-123',
        userId: 'user-123',
        balance: 10000,
        status: 'active',
      };

      mockGetAccountById.mockResolvedValueOnce(mockAccount);
      mockUpdateAccountBalance.mockResolvedValueOnce({
        ...mockAccount,
        balance: 9500,
      });

      const result = await transactionService.processDistribution('dist-124');

      expect(result.status).toBe('distributed');
      expect(mockUpdateAccountBalance).toHaveBeenCalled();
    });
  });

  describe('getTransactionById', () => {
    it('should retrieve a transaction by ID', async () => {
      const mockAccount = {
        id: 'account-123',
        userId: 'user-123',
        balance: 1000,
        status: 'active',
      };

      mockGetAccountById.mockResolvedValueOnce(mockAccount);
      mockUpdateAccountBalance.mockResolvedValueOnce({
        ...mockAccount,
        balance: 1500,
      });

      const created = await transactionService.createDeposit({
        accountId: 'account-123',
        amount: 500,
      });

      const result = await transactionService.getTransactionById(created.id);

      expect(result).toBeDefined();
      expect(result?.id).toBe(created.id);
    });

    it('should return null for non-existent transaction', async () => {
      const result = await transactionService.getTransactionById('non-existent');

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

  describe('getUserTransactions', () => {
    it('should retrieve all transactions for a user', async () => {
      const result = await transactionService.getUserTransactions('user-123');

      expect(result).toBeDefined();
      expect(Array.isArray(result)).toBe(true);
    });

    it('should filter by account', async () => {
      const result = await transactionService.getUserTransactions('user-123', {
        accountId: 'account-123',
      });

      expect(result.every(tx => tx.accountId === 'account-123')).toBe(true);
    });

    it('should paginate results', async () => {
      const page1 = await transactionService.getUserTransactions('user-123', {
        limit: 10,
        offset: 0,
      });

      const page2 = await transactionService.getUserTransactions('user-123', {
        limit: 10,
        offset: 10,
      });

      expect(page1.length).toBeLessThanOrEqual(10);
      expect(page2.length).toBeLessThanOrEqual(10);
    });
  });

  describe('getTransactionStatistics', () => {
    it('should calculate transaction statistics', async () => {
      const result = await transactionService.getTransactionStatistics('account-123');

      expect(result).toBeDefined();
      expect(result).toHaveProperty('totalDeposits');
      expect(result).toHaveProperty('totalWithdrawals');
      expect(result).toHaveProperty('totalEarnings');
      expect(result).toHaveProperty('totalFees');
      expect(result).toHaveProperty('netFlow');
    });

    it('should filter statistics by date range', async () => {
      const result = await transactionService.getTransactionStatistics('account-123', {
        startDate: new Date('2024-01-01'),
        endDate: new Date('2024-12-31'),
      });

      expect(result).toBeDefined();
    });
  });
});
