// Mock dependencies - define mocks inline to avoid hoisting issues
jest.mock('../../../models', () => ({
  Rental: {
    findAll: jest.fn(),
    update: jest.fn()
  },
  User: jest.fn(),
  Item: jest.fn()
}));

jest.mock('../../../services/stripeService', () => ({
  createTransfer: jest.fn()
}));

jest.mock('sequelize', () => ({
  Op: {
    not: 'not'
  }
}));

const PayoutService = require('../../../services/payoutService');
const { Rental, User, Item } = require('../../../models');
const StripeService = require('../../../services/stripeService');

// Get references to mocks after importing
const mockRentalFindAll = Rental.findAll;
const mockRentalUpdate = Rental.update;
const mockUserModel = User;
const mockItemModel = Item;
const mockCreateTransfer = StripeService.createTransfer;

describe('PayoutService', () => {
  let consoleSpy, consoleErrorSpy;

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

    // Set up console spies
    consoleSpy = jest.spyOn(console, 'log').mockImplementation();
    consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
  });

  afterEach(() => {
    consoleSpy.mockRestore();
    consoleErrorSpy.mockRestore();
  });

  describe('getEligiblePayouts', () => {
    it('should return eligible rentals for payout', async () => {
      const mockRentals = [
        {
          id: 1,
          status: 'completed',
          paymentStatus: 'paid',
          payoutStatus: 'pending',
          owner: {
            id: 1,
            stripeConnectedAccountId: 'acct_123'
          }
        },
        {
          id: 2,
          status: 'completed',
          paymentStatus: 'paid',
          payoutStatus: 'pending',
          owner: {
            id: 2,
            stripeConnectedAccountId: 'acct_456'
          }
        }
      ];

      mockRentalFindAll.mockResolvedValue(mockRentals);

      const result = await PayoutService.getEligiblePayouts();

      expect(mockRentalFindAll).toHaveBeenCalledWith({
        where: {
          status: 'completed',
          paymentStatus: 'paid',
          payoutStatus: 'pending'
        },
        include: [
          {
            model: mockUserModel,
            as: 'owner',
            where: {
              stripeConnectedAccountId: {
                'not': null
              }
            }
          },
          {
            model: mockItemModel,
            as: 'item'
          }
        ]
      });

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

    it('should handle database errors', async () => {
      const dbError = new Error('Database connection failed');
      mockRentalFindAll.mockRejectedValue(dbError);

      await expect(PayoutService.getEligiblePayouts()).rejects.toThrow('Database connection failed');

      expect(consoleErrorSpy).toHaveBeenCalledWith('Error getting eligible payouts:', dbError);
    });

    it('should return empty array when no eligible rentals found', async () => {
      mockRentalFindAll.mockResolvedValue([]);

      const result = await PayoutService.getEligiblePayouts();

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

  describe('processRentalPayout', () => {
    let mockRental;

    beforeEach(() => {
      mockRental = {
        id: 1,
        ownerId: 2,
        payoutStatus: 'pending',
        payoutAmount: 9500, // $95.00
        totalAmount: 10000, // $100.00
        platformFee: 500, // $5.00
        startDateTime: new Date('2023-01-01T10:00:00Z'),
        endDateTime: new Date('2023-01-02T10:00:00Z'),
        owner: {
          id: 2,
          stripeConnectedAccountId: 'acct_123'
        },
        update: jest.fn().mockResolvedValue(true)
      };
    });

    describe('Validation', () => {
      it('should throw error when owner has no connected Stripe account', async () => {
        mockRental.owner.stripeConnectedAccountId = null;

        await expect(PayoutService.processRentalPayout(mockRental))
          .rejects.toThrow('Owner does not have a connected Stripe account');
      });

      it('should throw error when owner is missing', async () => {
        mockRental.owner = null;

        await expect(PayoutService.processRentalPayout(mockRental))
          .rejects.toThrow('Owner does not have a connected Stripe account');
      });

      it('should throw error when payout already processed', async () => {
        mockRental.payoutStatus = 'completed';

        await expect(PayoutService.processRentalPayout(mockRental))
          .rejects.toThrow('Rental payout has already been processed');
      });

      it('should throw error when payout amount is invalid', async () => {
        mockRental.payoutAmount = 0;

        await expect(PayoutService.processRentalPayout(mockRental))
          .rejects.toThrow('Invalid payout amount');
      });

      it('should throw error when payout amount is negative', async () => {
        mockRental.payoutAmount = -100;

        await expect(PayoutService.processRentalPayout(mockRental))
          .rejects.toThrow('Invalid payout amount');
      });

      it('should throw error when payout amount is null', async () => {
        mockRental.payoutAmount = null;

        await expect(PayoutService.processRentalPayout(mockRental))
          .rejects.toThrow('Invalid payout amount');
      });
    });

    describe('Successful processing', () => {
      beforeEach(() => {
        mockCreateTransfer.mockResolvedValue({
          id: 'tr_123456789',
          amount: 9500,
          destination: 'acct_123'
        });
      });

      it('should successfully process a rental payout', async () => {
        const result = await PayoutService.processRentalPayout(mockRental);

        // Verify Stripe transfer creation
        expect(mockCreateTransfer).toHaveBeenCalledWith({
          amount: 9500,
          destination: 'acct_123',
          metadata: {
            rentalId: 1,
            ownerId: 2,
            totalAmount: '10000',
            platformFee: '500',
            startDateTime: '2023-01-01T10:00:00.000Z',
            endDateTime: '2023-01-02T10:00:00.000Z'
          }
        });

        // Verify status update to completed
        expect(mockRental.update).toHaveBeenCalledWith({
          payoutStatus: 'completed',
          payoutProcessedAt: expect.any(Date),
          stripeTransferId: 'tr_123456789'
        });

        // Verify success log
        expect(consoleSpy).toHaveBeenCalledWith(
          'Payout completed for rental 1: $9500 to acct_123'
        );

        // Verify return value
        expect(result).toEqual({
          success: true,
          transferId: 'tr_123456789',
          amount: 9500
        });
      });

      it('should handle successful payout with different amounts', async () => {
        mockRental.payoutAmount = 15000;
        mockRental.totalAmount = 16000;
        mockRental.platformFee = 1000;

        mockCreateTransfer.mockResolvedValue({
          id: 'tr_987654321',
          amount: 15000,
          destination: 'acct_123'
        });

        const result = await PayoutService.processRentalPayout(mockRental);

        expect(mockCreateTransfer).toHaveBeenCalledWith({
          amount: 15000,
          destination: 'acct_123',
          metadata: expect.objectContaining({
            totalAmount: '16000',
            platformFee: '1000'
          })
        });

        expect(result.amount).toBe(15000);
        expect(result.transferId).toBe('tr_987654321');
      });
    });

    describe('Error handling', () => {
      it('should handle Stripe transfer creation errors', async () => {
        const stripeError = new Error('Stripe transfer failed');
        mockCreateTransfer.mockRejectedValue(stripeError);

        await expect(PayoutService.processRentalPayout(mockRental))
          .rejects.toThrow('Stripe transfer failed');

        // Verify failure status was set
        expect(mockRental.update).toHaveBeenCalledWith({
          payoutStatus: 'failed'
        });

        expect(consoleErrorSpy).toHaveBeenCalledWith(
          'Error processing payout for rental 1:',
          stripeError
        );
      });

      it('should handle database update errors during processing', async () => {
        // Stripe succeeds but database update fails
        mockCreateTransfer.mockResolvedValue({
          id: 'tr_123456789',
          amount: 9500
        });
        const dbError = new Error('Database update failed');
        mockRental.update.mockRejectedValueOnce(dbError);

        await expect(PayoutService.processRentalPayout(mockRental))
          .rejects.toThrow('Database update failed');

        expect(consoleErrorSpy).toHaveBeenCalledWith(
          'Error processing payout for rental 1:',
          dbError
        );
      });

      it('should handle database update errors during completion', async () => {
        mockCreateTransfer.mockResolvedValue({
          id: 'tr_123456789',
          amount: 9500
        });

        const dbError = new Error('Database completion update failed');
        mockRental.update.mockRejectedValueOnce(dbError);

        await expect(PayoutService.processRentalPayout(mockRental))
          .rejects.toThrow('Database completion update failed');

        expect(mockCreateTransfer).toHaveBeenCalled();
        expect(consoleErrorSpy).toHaveBeenCalledWith(
          'Error processing payout for rental 1:',
          dbError
        );
      });

      it('should handle failure status update errors gracefully', async () => {
        const stripeError = new Error('Stripe transfer failed');
        const updateError = new Error('Update failed status failed');

        mockCreateTransfer.mockRejectedValue(stripeError);
        mockRental.update.mockRejectedValueOnce(updateError);

        // The service will throw the update error since it happens in the catch block
        await expect(PayoutService.processRentalPayout(mockRental))
          .rejects.toThrow('Update failed status failed');

        // Should still attempt to update to failed status
        expect(mockRental.update).toHaveBeenCalledWith({
          payoutStatus: 'failed'
        });
      });
    });
  });

  describe('processAllEligiblePayouts', () => {
    beforeEach(() => {
      jest.spyOn(PayoutService, 'getEligiblePayouts');
      jest.spyOn(PayoutService, 'processRentalPayout');
    });

    afterEach(() => {
      PayoutService.getEligiblePayouts.mockRestore();
      PayoutService.processRentalPayout.mockRestore();
    });

    it('should process all eligible payouts successfully', async () => {
      const mockRentals = [
        { id: 1, payoutAmount: 9500 },
        { id: 2, payoutAmount: 7500 }
      ];

      PayoutService.getEligiblePayouts.mockResolvedValue(mockRentals);
      PayoutService.processRentalPayout
        .mockResolvedValueOnce({
          success: true,
          transferId: 'tr_123',
          amount: 9500
        })
        .mockResolvedValueOnce({
          success: true,
          transferId: 'tr_456',
          amount: 7500
        });

      const result = await PayoutService.processAllEligiblePayouts();

      expect(consoleSpy).toHaveBeenCalledWith('Found 2 eligible rentals for payout');
      expect(consoleSpy).toHaveBeenCalledWith('Payout processing complete: 2 successful, 0 failed');

      expect(result).toEqual({
        successful: [
          { rentalId: 1, amount: 9500, transferId: 'tr_123' },
          { rentalId: 2, amount: 7500, transferId: 'tr_456' }
        ],
        failed: [],
        totalProcessed: 2
      });
    });

    it('should handle mixed success and failure results', async () => {
      const mockRentals = [
        { id: 1, payoutAmount: 9500 },
        { id: 2, payoutAmount: 7500 },
        { id: 3, payoutAmount: 12000 }
      ];

      PayoutService.getEligiblePayouts.mockResolvedValue(mockRentals);
      PayoutService.processRentalPayout
        .mockResolvedValueOnce({
          success: true,
          transferId: 'tr_123',
          amount: 9500
        })
        .mockRejectedValueOnce(new Error('Stripe account suspended'))
        .mockResolvedValueOnce({
          success: true,
          transferId: 'tr_789',
          amount: 12000
        });

      const result = await PayoutService.processAllEligiblePayouts();

      expect(consoleSpy).toHaveBeenCalledWith('Found 3 eligible rentals for payout');
      expect(consoleSpy).toHaveBeenCalledWith('Payout processing complete: 2 successful, 1 failed');

      expect(result).toEqual({
        successful: [
          { rentalId: 1, amount: 9500, transferId: 'tr_123' },
          { rentalId: 3, amount: 12000, transferId: 'tr_789' }
        ],
        failed: [
          { rentalId: 2, error: 'Stripe account suspended' }
        ],
        totalProcessed: 3
      });
    });

    it('should handle no eligible payouts', async () => {
      PayoutService.getEligiblePayouts.mockResolvedValue([]);

      const result = await PayoutService.processAllEligiblePayouts();

      expect(consoleSpy).toHaveBeenCalledWith('Found 0 eligible rentals for payout');
      expect(consoleSpy).toHaveBeenCalledWith('Payout processing complete: 0 successful, 0 failed');

      expect(result).toEqual({
        successful: [],
        failed: [],
        totalProcessed: 0
      });

      expect(PayoutService.processRentalPayout).not.toHaveBeenCalled();
    });

    it('should handle errors in getEligiblePayouts', async () => {
      const dbError = new Error('Database connection failed');
      PayoutService.getEligiblePayouts.mockRejectedValue(dbError);

      await expect(PayoutService.processAllEligiblePayouts())
        .rejects.toThrow('Database connection failed');

      expect(consoleErrorSpy).toHaveBeenCalledWith(
        'Error processing all eligible payouts:',
        dbError
      );
    });

    it('should handle all payouts failing', async () => {
      const mockRentals = [
        { id: 1, payoutAmount: 9500 },
        { id: 2, payoutAmount: 7500 }
      ];

      PayoutService.getEligiblePayouts.mockResolvedValue(mockRentals);
      PayoutService.processRentalPayout
        .mockRejectedValueOnce(new Error('Transfer failed'))
        .mockRejectedValueOnce(new Error('Account not found'));

      const result = await PayoutService.processAllEligiblePayouts();

      expect(result).toEqual({
        successful: [],
        failed: [
          { rentalId: 1, error: 'Transfer failed' },
          { rentalId: 2, error: 'Account not found' }
        ],
        totalProcessed: 2
      });
    });
  });

  describe('retryFailedPayouts', () => {
    beforeEach(() => {
      jest.spyOn(PayoutService, 'processRentalPayout');
    });

    afterEach(() => {
      PayoutService.processRentalPayout.mockRestore();
    });

    it('should retry failed payouts successfully', async () => {
      const mockFailedRentals = [
        {
          id: 1,
          payoutAmount: 9500,
          update: jest.fn().mockResolvedValue(true)
        },
        {
          id: 2,
          payoutAmount: 7500,
          update: jest.fn().mockResolvedValue(true)
        }
      ];

      mockRentalFindAll.mockResolvedValue(mockFailedRentals);
      PayoutService.processRentalPayout
        .mockResolvedValueOnce({
          success: true,
          transferId: 'tr_retry_123',
          amount: 9500
        })
        .mockResolvedValueOnce({
          success: true,
          transferId: 'tr_retry_456',
          amount: 7500
        });

      const result = await PayoutService.retryFailedPayouts();

      // Verify query for failed rentals
      expect(mockRentalFindAll).toHaveBeenCalledWith({
        where: {
          status: 'completed',
          paymentStatus: 'paid',
          payoutStatus: 'failed'
        },
        include: [
          {
            model: mockUserModel,
            as: 'owner',
            where: {
              stripeConnectedAccountId: {
                'not': null
              }
            }
          },
          {
            model: mockItemModel,
            as: 'item'
          }
        ]
      });

      // Verify status reset to pending
      expect(mockFailedRentals[0].update).toHaveBeenCalledWith({ payoutStatus: 'pending' });
      expect(mockFailedRentals[1].update).toHaveBeenCalledWith({ payoutStatus: 'pending' });

      // Verify processing attempts
      expect(PayoutService.processRentalPayout).toHaveBeenCalledWith(mockFailedRentals[0]);
      expect(PayoutService.processRentalPayout).toHaveBeenCalledWith(mockFailedRentals[1]);

      // Verify logs
      expect(consoleSpy).toHaveBeenCalledWith('Found 2 failed payouts to retry');
      expect(consoleSpy).toHaveBeenCalledWith('Retry processing complete: 2 successful, 0 failed');

      // Verify result
      expect(result).toEqual({
        successful: [
          { rentalId: 1, amount: 9500, transferId: 'tr_retry_123' },
          { rentalId: 2, amount: 7500, transferId: 'tr_retry_456' }
        ],
        failed: [],
        totalProcessed: 2
      });
    });

    it('should handle mixed retry results', async () => {
      const mockFailedRentals = [
        {
          id: 1,
          payoutAmount: 9500,
          update: jest.fn().mockResolvedValue(true)
        },
        {
          id: 2,
          payoutAmount: 7500,
          update: jest.fn().mockResolvedValue(true)
        }
      ];

      mockRentalFindAll.mockResolvedValue(mockFailedRentals);
      PayoutService.processRentalPayout
        .mockResolvedValueOnce({
          success: true,
          transferId: 'tr_retry_123',
          amount: 9500
        })
        .mockRejectedValueOnce(new Error('Still failing'));

      const result = await PayoutService.retryFailedPayouts();

      expect(result).toEqual({
        successful: [
          { rentalId: 1, amount: 9500, transferId: 'tr_retry_123' }
        ],
        failed: [
          { rentalId: 2, error: 'Still failing' }
        ],
        totalProcessed: 2
      });
    });

    it('should handle no failed payouts to retry', async () => {
      mockRentalFindAll.mockResolvedValue([]);

      const result = await PayoutService.retryFailedPayouts();

      expect(consoleSpy).toHaveBeenCalledWith('Found 0 failed payouts to retry');
      expect(consoleSpy).toHaveBeenCalledWith('Retry processing complete: 0 successful, 0 failed');

      expect(result).toEqual({
        successful: [],
        failed: [],
        totalProcessed: 0
      });

      expect(PayoutService.processRentalPayout).not.toHaveBeenCalled();
    });

    it('should handle errors in finding failed rentals', async () => {
      const dbError = new Error('Database query failed');
      mockRentalFindAll.mockRejectedValue(dbError);

      await expect(PayoutService.retryFailedPayouts())
        .rejects.toThrow('Database query failed');

      expect(consoleErrorSpy).toHaveBeenCalledWith(
        'Error retrying failed payouts:',
        dbError
      );
    });

    it('should handle status reset errors', async () => {
      const mockFailedRentals = [
        {
          id: 1,
          payoutAmount: 9500,
          update: jest.fn().mockRejectedValue(new Error('Status reset failed'))
        }
      ];

      mockRentalFindAll.mockResolvedValue(mockFailedRentals);

      const result = await PayoutService.retryFailedPayouts();

      expect(result.failed).toEqual([
        { rentalId: 1, error: 'Status reset failed' }
      ]);

      expect(PayoutService.processRentalPayout).not.toHaveBeenCalled();
    });
  });

  describe('Error logging', () => {
    it('should log errors with rental context in processRentalPayout', async () => {
      const mockRental = {
        id: 123,
        payoutStatus: 'pending',
        payoutAmount: 9500,
        owner: {
          stripeConnectedAccountId: 'acct_123'
        },
        update: jest.fn().mockRejectedValue(new Error('Update failed'))
      };

      await expect(PayoutService.processRentalPayout(mockRental))
        .rejects.toThrow('Update failed');

      expect(consoleErrorSpy).toHaveBeenCalledWith(
        'Error processing payout for rental 123:',
        expect.any(Error)
      );
    });

    it('should log aggregate results in processAllEligiblePayouts', async () => {
      jest.spyOn(PayoutService, 'getEligiblePayouts').mockResolvedValue([
        { id: 1 }, { id: 2 }, { id: 3 }
      ]);
      jest.spyOn(PayoutService, 'processRentalPayout')
        .mockResolvedValueOnce({ amount: 100, transferId: 'tr_1' })
        .mockRejectedValueOnce(new Error('Failed'))
        .mockResolvedValueOnce({ amount: 300, transferId: 'tr_3' });

      await PayoutService.processAllEligiblePayouts();

      expect(consoleSpy).toHaveBeenCalledWith('Found 3 eligible rentals for payout');
      expect(consoleSpy).toHaveBeenCalledWith('Payout processing complete: 2 successful, 1 failed');

      PayoutService.getEligiblePayouts.mockRestore();
      PayoutService.processRentalPayout.mockRestore();
    });
  });

  describe('Edge cases', () => {
    it('should handle rental with undefined owner', async () => {
      const mockRental = {
        id: 1,
        payoutStatus: 'pending',
        payoutAmount: 9500,
        owner: undefined,
        update: jest.fn()
      };

      await expect(PayoutService.processRentalPayout(mockRental))
        .rejects.toThrow('Owner does not have a connected Stripe account');
    });

    it('should handle rental with empty string Stripe account ID', async () => {
      const mockRental = {
        id: 1,
        payoutStatus: 'pending',
        payoutAmount: 9500,
        owner: {
          stripeConnectedAccountId: ''
        },
        update: jest.fn()
      };

      await expect(PayoutService.processRentalPayout(mockRental))
        .rejects.toThrow('Owner does not have a connected Stripe account');
    });

    it('should handle very large payout amounts', async () => {
      const mockRental = {
        id: 1,
        ownerId: 2,
        payoutStatus: 'pending',
        payoutAmount: 999999999, // Very large amount
        totalAmount: 1000000000,
        platformFee: 1,
        startDateTime: new Date('2023-01-01T10:00:00Z'),
        endDateTime: new Date('2023-01-02T10:00:00Z'),
        owner: {
          stripeConnectedAccountId: 'acct_123'
        },
        update: jest.fn().mockResolvedValue(true)
      };

      mockCreateTransfer.mockResolvedValue({
        id: 'tr_large_amount',
        amount: 999999999
      });

      const result = await PayoutService.processRentalPayout(mockRental);

      expect(mockCreateTransfer).toHaveBeenCalledWith({
        amount: 999999999,
        destination: 'acct_123',
        metadata: expect.objectContaining({
          totalAmount: '1000000000',
          platformFee: '1'
        })
      });

      expect(result.amount).toBe(999999999);
    });
  });
});