// Mock dependencies
const mockRentalFindByPk = jest.fn();
const mockRentalUpdate = jest.fn();
const mockCreateRefund = jest.fn();

jest.mock('../../../models', () => ({
  Rental: {
    findByPk: mockRentalFindByPk
  }
}));

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

const RefundService = require('../../../services/refundService');

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

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

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

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

  describe('calculateRefundAmount', () => {
    const baseRental = {
      totalAmount: 100.00,
      startDateTime: new Date('2023-12-01T10:00:00Z')
    };

    describe('Owner cancellation', () => {
      it('should return 100% refund when cancelled by owner', () => {
        const result = RefundService.calculateRefundAmount(baseRental, 'owner');

        expect(result).toEqual({
          refundAmount: 100.00,
          refundPercentage: 1.0,
          reason: 'Full refund - cancelled by owner'
        });
      });

      it('should handle decimal amounts correctly for owner cancellation', () => {
        const rental = { ...baseRental, totalAmount: 125.75 };
        const result = RefundService.calculateRefundAmount(rental, 'owner');

        expect(result).toEqual({
          refundAmount: 125.75,
          refundPercentage: 1.0,
          reason: 'Full refund - cancelled by owner'
        });
      });
    });

    describe('Renter cancellation', () => {
      it('should return 0% refund when cancelled within 24 hours', () => {
        // Use fake timers to set the current time
        jest.useFakeTimers();
        jest.setSystemTime(new Date('2023-11-30T15:00:00Z')); // 19 hours before start

        const result = RefundService.calculateRefundAmount(baseRental, 'renter');

        expect(result).toEqual({
          refundAmount: 0.00,
          refundPercentage: 0.0,
          reason: 'No refund - cancelled within 24 hours of start time'
        });

        jest.useRealTimers();
      });

      it('should return 50% refund when cancelled between 24-48 hours', () => {
        // Use fake timers to set the current time
        jest.useFakeTimers();
        jest.setSystemTime(new Date('2023-11-29T15:00:00Z')); // 43 hours before start

        const result = RefundService.calculateRefundAmount(baseRental, 'renter');

        expect(result).toEqual({
          refundAmount: 50.00,
          refundPercentage: 0.5,
          reason: '50% refund - cancelled between 24-48 hours of start time'
        });

        jest.useRealTimers();
      });

      it('should return 100% refund when cancelled more than 48 hours before', () => {
        // Use fake timers to set the current time
        jest.useFakeTimers();
        jest.setSystemTime(new Date('2023-11-28T15:00:00Z')); // 67 hours before start

        const result = RefundService.calculateRefundAmount(baseRental, 'renter');

        expect(result).toEqual({
          refundAmount: 100.00,
          refundPercentage: 1.0,
          reason: 'Full refund - cancelled more than 48 hours before start time'
        });

        jest.useRealTimers();
      });

      it('should handle decimal calculations correctly for 50% refund', () => {
        const rental = { ...baseRental, totalAmount: 127.33 };
        jest.useFakeTimers();
        jest.setSystemTime(new Date('2023-11-29T15:00:00Z')); // 43 hours before start

        const result = RefundService.calculateRefundAmount(rental, 'renter');

        expect(result).toEqual({
          refundAmount: 63.66, // 127.33 * 0.5 = 63.665, rounded to 63.66
          refundPercentage: 0.5,
          reason: '50% refund - cancelled between 24-48 hours of start time'
        });

        jest.useRealTimers();
      });

      it('should handle edge case exactly at 24 hours', () => {
        jest.useFakeTimers();
        jest.setSystemTime(new Date('2023-11-30T10:00:00Z')); // exactly 24 hours before start

        const result = RefundService.calculateRefundAmount(baseRental, 'renter');

        expect(result).toEqual({
          refundAmount: 50.00,
          refundPercentage: 0.5,
          reason: '50% refund - cancelled between 24-48 hours of start time'
        });

        jest.useRealTimers();
      });

      it('should handle edge case exactly at 48 hours', () => {
        jest.useFakeTimers();
        jest.setSystemTime(new Date('2023-11-29T10:00:00Z')); // exactly 48 hours before start

        const result = RefundService.calculateRefundAmount(baseRental, 'renter');

        expect(result).toEqual({
          refundAmount: 100.00,
          refundPercentage: 1.0,
          reason: 'Full refund - cancelled more than 48 hours before start time'
        });

        jest.useRealTimers();
      });
    });

    describe('Edge cases', () => {
      it('should handle zero total amount', () => {
        const rental = { ...baseRental, totalAmount: 0 };
        const result = RefundService.calculateRefundAmount(rental, 'owner');

        expect(result).toEqual({
          refundAmount: 0.00,
          refundPercentage: 1.0,
          reason: 'Full refund - cancelled by owner'
        });
      });

      it('should handle unknown cancelledBy value', () => {
        const result = RefundService.calculateRefundAmount(baseRental, 'unknown');

        expect(result).toEqual({
          refundAmount: 0.00,
          refundPercentage: 0,
          reason: ''
        });
      });

      it('should handle past rental start time for renter cancellation', () => {
        jest.useFakeTimers();
        jest.setSystemTime(new Date('2023-12-02T10:00:00Z')); // 24 hours after start

        const result = RefundService.calculateRefundAmount(baseRental, 'renter');

        expect(result).toEqual({
          refundAmount: 0.00,
          refundPercentage: 0.0,
          reason: 'No refund - cancelled within 24 hours of start time'
        });

        jest.useRealTimers();
      });
    });
  });

  describe('validateCancellationEligibility', () => {
    const baseRental = {
      id: 1,
      renterId: 100,
      ownerId: 200,
      status: 'pending',
      paymentStatus: 'paid'
    };

    describe('Status validation', () => {
      it('should reject cancellation for already cancelled rental', () => {
        const rental = { ...baseRental, status: 'cancelled' };
        const result = RefundService.validateCancellationEligibility(rental, 100);

        expect(result).toEqual({
          canCancel: false,
          reason: 'Rental is already cancelled',
          cancelledBy: null
        });
      });

      it('should reject cancellation for completed rental', () => {
        const rental = { ...baseRental, status: 'completed' };
        const result = RefundService.validateCancellationEligibility(rental, 100);

        expect(result).toEqual({
          canCancel: false,
          reason: 'Cannot cancel completed rental',
          cancelledBy: null
        });
      });

      it('should reject cancellation for active rental (computed from confirmed + past start)', () => {
        // Active status is now computed: confirmed + startDateTime in the past
        const pastDate = new Date();
        pastDate.setHours(pastDate.getHours() - 1); // 1 hour ago
        const rental = { ...baseRental, status: 'confirmed', startDateTime: pastDate };
        const result = RefundService.validateCancellationEligibility(rental, 100);

        expect(result).toEqual({
          canCancel: false,
          reason: 'Cannot cancel active rental',
          cancelledBy: null
        });
      });
    });

    describe('Authorization validation', () => {
      it('should allow renter to cancel', () => {
        const result = RefundService.validateCancellationEligibility(baseRental, 100);

        expect(result).toEqual({
          canCancel: true,
          reason: 'Cancellation allowed',
          cancelledBy: 'renter'
        });
      });

      it('should allow owner to cancel', () => {
        const result = RefundService.validateCancellationEligibility(baseRental, 200);

        expect(result).toEqual({
          canCancel: true,
          reason: 'Cancellation allowed',
          cancelledBy: 'owner'
        });
      });

      it('should reject unauthorized user', () => {
        const result = RefundService.validateCancellationEligibility(baseRental, 999);

        expect(result).toEqual({
          canCancel: false,
          reason: 'You are not authorized to cancel this rental',
          cancelledBy: null
        });
      });
    });

    describe('Payment status validation', () => {
      it('should reject cancellation for confirmed rental with unpaid status', () => {
        // Confirmed rentals require payment to be settled
        const rental = { ...baseRental, status: 'confirmed', paymentStatus: 'pending' };
        const result = RefundService.validateCancellationEligibility(rental, 100);

        expect(result).toEqual({
          canCancel: false,
          reason: 'Cannot cancel rental that hasn\'t been paid',
          cancelledBy: null
        });
      });

      it('should reject cancellation for confirmed rental with failed payment', () => {
        const rental = { ...baseRental, status: 'confirmed', paymentStatus: 'failed' };
        const result = RefundService.validateCancellationEligibility(rental, 100);

        expect(result).toEqual({
          canCancel: false,
          reason: 'Cannot cancel rental that hasn\'t been paid',
          cancelledBy: null
        });
      });

      it('should allow cancellation for free rental with not_required payment status', () => {
        const rental = { ...baseRental, paymentStatus: 'not_required' };
        const result = RefundService.validateCancellationEligibility(rental, 100);

        expect(result).toEqual({
          canCancel: true,
          reason: 'Cancellation allowed',
          cancelledBy: 'renter'
        });
      });
    });

    describe('Pending rental cancellation (before owner approval)', () => {
      it('should allow renter to cancel pending rental even with pending payment', () => {
        // Pending rentals can be cancelled before owner approval, no payment processed yet
        const rental = { ...baseRental, status: 'pending', paymentStatus: 'pending' };
        const result = RefundService.validateCancellationEligibility(rental, 100);

        expect(result).toEqual({
          canCancel: true,
          reason: 'Cancellation allowed',
          cancelledBy: 'renter'
        });
      });

      it('should allow owner to cancel pending rental', () => {
        const rental = { ...baseRental, status: 'pending', paymentStatus: 'pending' };
        const result = RefundService.validateCancellationEligibility(rental, 200);

        expect(result).toEqual({
          canCancel: true,
          reason: 'Cancellation allowed',
          cancelledBy: 'owner'
        });
      });
    });

    describe('Edge cases', () => {
      it('should handle string user IDs that don\'t match', () => {
        const result = RefundService.validateCancellationEligibility(baseRental, '100');

        expect(result).toEqual({
          canCancel: false,
          reason: 'You are not authorized to cancel this rental',
          cancelledBy: null
        });
      });

      it('should handle null user ID', () => {
        const result = RefundService.validateCancellationEligibility(baseRental, null);

        expect(result).toEqual({
          canCancel: false,
          reason: 'You are not authorized to cancel this rental',
          cancelledBy: null
        });
      });
    });
  });

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

    beforeEach(() => {
      mockRental = {
        id: 1,
        renterId: 100,
        ownerId: 200,
        status: 'pending',
        paymentStatus: 'paid',
        totalAmount: 100.00,
        stripePaymentIntentId: 'pi_123456789',
        startDateTime: new Date('2023-12-01T10:00:00Z'),
        update: mockRentalUpdate
      };

      mockRentalFindByPk.mockResolvedValue(mockRental);
      mockRentalUpdate.mockResolvedValue(mockRental);
    });

    describe('Rental not found', () => {
      it('should throw error when rental not found', async () => {
        mockRentalFindByPk.mockResolvedValue(null);

        await expect(RefundService.processCancellation('999', 100))
          .rejects.toThrow('Rental not found');

        expect(mockRentalFindByPk).toHaveBeenCalledWith('999');
      });
    });

    describe('Validation failures', () => {
      it('should throw error for invalid cancellation', async () => {
        mockRental.status = 'cancelled';

        await expect(RefundService.processCancellation(1, 100))
          .rejects.toThrow('Rental is already cancelled');
      });

      it('should throw error for unauthorized user', async () => {
        await expect(RefundService.processCancellation(1, 999))
          .rejects.toThrow('You are not authorized to cancel this rental');
      });
    });

    describe('Successful cancellation with refund', () => {
      beforeEach(() => {
        // Set time to more than 48 hours before start for full refund
        jest.useFakeTimers();
        jest.setSystemTime(new Date('2023-11-28T10:00:00Z'));

        mockCreateRefund.mockResolvedValue({
          id: 're_123456789',
          amount: 10000 // Stripe uses cents
        });
      });

      afterEach(() => {
        jest.useRealTimers();
      });

      it('should process owner cancellation with full refund', async () => {
        const result = await RefundService.processCancellation(1, 200, 'Owner needs to cancel');

        // Verify Stripe refund was created
        expect(mockCreateRefund).toHaveBeenCalledWith({
          paymentIntentId: 'pi_123456789',
          amount: 100.00,
          metadata: {
            rentalId: 1,
            cancelledBy: 'owner',
            refundReason: 'Full refund - cancelled by owner'
          }
        });

        // Verify rental was updated
        expect(mockRentalUpdate).toHaveBeenCalledWith({
          status: 'cancelled',
          cancelledBy: 'owner',
          cancelledAt: expect.any(Date),
          refundAmount: 100.00,
          refundProcessedAt: expect.any(Date),
          refundReason: 'Owner needs to cancel',
          stripeRefundId: 're_123456789',
          payoutStatus: 'pending'
        });

        expect(result).toEqual({
          rental: mockRental,
          refund: {
            amount: 100.00,
            percentage: 1.0,
            reason: 'Full refund - cancelled by owner',
            processed: true,
            stripeRefundId: 're_123456789'
          }
        });
      });

      it('should process renter cancellation with partial refund', async () => {
        // Set time to 36 hours before start for 50% refund
        jest.useRealTimers(); // Reset timers first
        jest.useFakeTimers();
        jest.setSystemTime(new Date('2023-11-29T22:00:00Z')); // 36 hours before start

        mockCreateRefund.mockResolvedValue({
          id: 're_partial',
          amount: 5000 // 50% in cents
        });

        const result = await RefundService.processCancellation(1, 100);

        expect(mockCreateRefund).toHaveBeenCalledWith({
          paymentIntentId: 'pi_123456789',
          amount: 50.00,
          metadata: {
            rentalId: 1,
            cancelledBy: 'renter',
            refundReason: '50% refund - cancelled between 24-48 hours of start time'
          }
        });

        expect(result.refund).toEqual({
          amount: 50.00,
          percentage: 0.5,
          reason: '50% refund - cancelled between 24-48 hours of start time',
          processed: true,
          stripeRefundId: 're_partial'
        });
      });
    });

    describe('No refund scenarios', () => {
      beforeEach(() => {
        // Set time to within 24 hours for no refund
        jest.useFakeTimers();
        jest.setSystemTime(new Date('2023-11-30T15:00:00Z'));
      });

      afterEach(() => {
        jest.useRealTimers();
      });

      it('should handle cancellation with no refund', async () => {
        const result = await RefundService.processCancellation(1, 100);

        // Verify no Stripe refund was attempted
        expect(mockCreateRefund).not.toHaveBeenCalled();

        // Verify rental was updated
        expect(mockRentalUpdate).toHaveBeenCalledWith({
          status: 'cancelled',
          cancelledBy: 'renter',
          cancelledAt: expect.any(Date),
          refundAmount: 0.00,
          refundProcessedAt: null,
          refundReason: 'No refund - cancelled within 24 hours of start time',
          stripeRefundId: null,
          payoutStatus: 'pending'
        });

        expect(result.refund).toEqual({
          amount: 0.00,
          percentage: 0.0,
          reason: 'No refund - cancelled within 24 hours of start time',
          processed: false,
          stripeRefundId: null
        });
      });

      it('should handle refund without payment intent ID', async () => {
        mockRental.stripePaymentIntentId = null;
        // Set to full refund scenario
        jest.useRealTimers();
        jest.useFakeTimers();
        jest.setSystemTime(new Date('2023-11-28T10:00:00Z'));

        const result = await RefundService.processCancellation(1, 200);

        expect(mockCreateRefund).not.toHaveBeenCalled();
        expect(consoleWarnSpy).toHaveBeenCalledWith(
          'Refund amount calculated but no payment intent ID for rental 1'
        );

        expect(result.refund).toEqual({
          amount: 100.00,
          percentage: 1.0,
          reason: 'Full refund - cancelled by owner',
          processed: false,
          stripeRefundId: null
        });
      });
    });

    describe('Pending rental cancellation (before owner approval)', () => {
      it('should process cancellation for pending rental without Stripe refund', async () => {
        // Pending rental with no payment processed yet
        mockRental.status = 'pending';
        mockRental.paymentStatus = 'pending';
        mockRental.stripePaymentIntentId = null;

        jest.useFakeTimers();
        jest.setSystemTime(new Date('2023-11-28T10:00:00Z'));

        const result = await RefundService.processCancellation(1, 100, 'Changed my mind');

        // No Stripe refund should be attempted
        expect(mockCreateRefund).not.toHaveBeenCalled();

        // Rental should be updated
        expect(mockRentalUpdate).toHaveBeenCalledWith({
          status: 'cancelled',
          cancelledBy: 'renter',
          cancelledAt: expect.any(Date),
          refundAmount: 100.00,
          refundProcessedAt: null,
          refundReason: 'Changed my mind',
          stripeRefundId: null,
          payoutStatus: 'pending'
        });

        expect(result.refund.processed).toBe(false);
        expect(result.refund.stripeRefundId).toBeNull();

        jest.useRealTimers();
      });
    });

    describe('Error handling', () => {
      beforeEach(() => {
        jest.useFakeTimers();
        jest.setSystemTime(new Date('2023-11-28T10:00:00Z'));
      });

      afterEach(() => {
        jest.useRealTimers();
      });

      it('should handle Stripe refund errors', async () => {
        const stripeError = new Error('Refund failed');
        mockCreateRefund.mockRejectedValue(stripeError);

        await expect(RefundService.processCancellation(1, 200))
          .rejects.toThrow('Failed to process refund: Refund failed');

        expect(consoleErrorSpy).toHaveBeenCalledWith(
          'Error processing Stripe refund:',
          stripeError
        );
      });

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

        mockCreateRefund.mockResolvedValue({
          id: 're_123456789'
        });

        await expect(RefundService.processCancellation(1, 200))
          .rejects.toThrow('Database update failed');
      });
    });
  });

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

    beforeEach(() => {
      mockRental = {
        id: 1,
        renterId: 100,
        ownerId: 200,
        status: 'pending',
        paymentStatus: 'paid',
        totalAmount: 150.00,
        startDateTime: new Date('2023-12-01T10:00:00Z')
      };

      mockRentalFindByPk.mockResolvedValue(mockRental);
    });

    describe('Successful preview', () => {
      it('should return owner cancellation preview', async () => {
        const result = await RefundService.getRefundPreview(1, 200);

        expect(result).toEqual({
          canCancel: true,
          cancelledBy: 'owner',
          refundAmount: 150.00,
          refundPercentage: 1.0,
          reason: 'Full refund - cancelled by owner',
          totalAmount: 150.00
        });
      });

      it('should return renter cancellation preview with partial refund', async () => {
        // Set time for 50% refund
        jest.useFakeTimers();
        jest.setSystemTime(new Date('2023-11-29T22:00:00Z')); // 36 hours before start

        const result = await RefundService.getRefundPreview(1, 100);

        expect(result).toEqual({
          canCancel: true,
          cancelledBy: 'renter',
          refundAmount: 75.00,
          refundPercentage: 0.5,
          reason: '50% refund - cancelled between 24-48 hours of start time',
          totalAmount: 150.00
        });

        jest.useRealTimers();
      });

      it('should return renter cancellation preview with no refund', async () => {
        // Set time for no refund
        jest.useFakeTimers();
        jest.setSystemTime(new Date('2023-11-30T15:00:00Z'));

        const result = await RefundService.getRefundPreview(1, 100);

        expect(result).toEqual({
          canCancel: true,
          cancelledBy: 'renter',
          refundAmount: 0.00,
          refundPercentage: 0.0,
          reason: 'No refund - cancelled within 24 hours of start time',
          totalAmount: 150.00
        });

        jest.useRealTimers();
      });
    });

    describe('Error cases', () => {
      it('should throw error when rental not found', async () => {
        mockRentalFindByPk.mockResolvedValue(null);

        await expect(RefundService.getRefundPreview('999', 100))
          .rejects.toThrow('Rental not found');
      });

      it('should throw error for invalid cancellation', async () => {
        mockRental.status = 'cancelled';

        await expect(RefundService.getRefundPreview(1, 100))
          .rejects.toThrow('Rental is already cancelled');
      });

      it('should throw error for unauthorized user', async () => {
        await expect(RefundService.getRefundPreview(1, 999))
          .rejects.toThrow('You are not authorized to cancel this rental');
      });
    });
  });

  describe('Edge cases and error scenarios', () => {
    it('should handle invalid rental IDs in processCancellation', async () => {
      mockRentalFindByPk.mockResolvedValue(null);

      await expect(RefundService.processCancellation('invalid', 100))
        .rejects.toThrow('Rental not found');
    });

    it('should handle very large refund amounts', async () => {
      const rental = {
        totalAmount: 999999.99,
        startDateTime: new Date('2023-12-01T10:00:00Z')
      };

      const result = RefundService.calculateRefundAmount(rental, 'owner');

      expect(result.refundAmount).toBe(999999.99);
      expect(result.refundPercentage).toBe(1.0);
    });

    it('should handle refund amount rounding edge cases', async () => {
      const rental = {
        totalAmount: 33.333,
        startDateTime: new Date('2023-12-01T10:00:00Z')
      };

      // Set time for 50% refund
      jest.useFakeTimers();
      jest.setSystemTime(new Date('2023-11-29T22:00:00Z')); // 36 hours before start

      const result = RefundService.calculateRefundAmount(rental, 'renter');

      expect(result.refundAmount).toBe(16.67); // 33.333 * 0.5 = 16.6665, rounded to 16.67
      expect(result.refundPercentage).toBe(0.5);

      jest.useRealTimers();
    });
  });
});