import { jest, describe, it, expect, beforeEach } from '@jest/globals';
import { createMockRepository } from '../../../__tests__/helpers.js';

// Mock factories for Stripe entities
function createMockStripeEvent(overrides: Record<string, any> = {}) {
  return {
    id: 'event-uuid-1',
    stripeEventId: 'evt_1234567890',
    eventType: 'customer.subscription.created',
    apiVersion: '2023-10-16',
    data: {
      object: {
        id: 'sub_123',
        customer: 'cus_123',
        status: 'active',
        current_period_start: Math.floor(Date.now() / 1000),
        current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
      },
    },
    processed: false,
    processedAt: null,
    retryCount: 0,
    errorMessage: null,
    createdAt: new Date(),
    updatedAt: new Date(),
    ...overrides,
  };
}

function createMockSubscription(overrides: Record<string, any> = {}) {
  return {
    id: 'subscription-uuid-1',
    tenantId: 'tenant-uuid-1',
    planId: 'plan-uuid-1',
    status: 'active',
    stripeCustomerId: 'cus_123',
    stripeSubscriptionId: 'sub_123',
    currentPeriodStart: new Date(),
    currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
    billingCycle: 'monthly',
    currentPrice: 499,
    createdAt: new Date(),
    updatedAt: new Date(),
    ...overrides,
  };
}

// Mock repositories
const mockEventRepository = createMockRepository();
const mockSubscriptionRepository = createMockRepository();

// Mock DataSource
const mockDataSource = {
  getRepository: jest.fn((entity: any) => {
    const entityName = entity.name || entity;
    if (entityName === 'StripeEvent') return mockEventRepository;
    if (entityName === 'TenantSubscription') return mockSubscriptionRepository;
    return mockEventRepository;
  }),
};

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

// Import after mocking
import { StripeWebhookService, StripeWebhookPayload } from '../services/stripe-webhook.service.js';

describe('StripeWebhookService', () => {
  let service: StripeWebhookService;

  beforeEach(() => {
    jest.clearAllMocks();
    service = new StripeWebhookService(mockDataSource as any);
  });

  describe('processWebhook', () => {
    it('should process a new webhook event successfully', async () => {
      const payload: StripeWebhookPayload = {
        id: 'evt_new_event',
        type: 'customer.subscription.created',
        api_version: '2023-10-16',
        data: {
          object: {
            id: 'sub_new',
            customer: 'cus_123',
            status: 'active',
            current_period_start: Math.floor(Date.now() / 1000),
            current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
          },
        },
        created: Math.floor(Date.now() / 1000),
        livemode: false,
      };

      const mockEvent = createMockStripeEvent({ stripeEventId: 'evt_new_event' });
      const mockSubscription = createMockSubscription();

      mockEventRepository.findOne.mockResolvedValue(null); // No existing event
      mockEventRepository.create.mockReturnValue(mockEvent);
      mockEventRepository.save.mockResolvedValue(mockEvent);
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSubscription);
      mockSubscriptionRepository.save.mockResolvedValue(mockSubscription);

      const result = await service.processWebhook(payload);

      expect(result.success).toBe(true);
      expect(result.message).toBe('Event processed successfully');
      expect(mockEventRepository.create).toHaveBeenCalled();
    });

    it('should return success for already processed event', async () => {
      const payload: StripeWebhookPayload = {
        id: 'evt_already_processed',
        type: 'customer.subscription.created',
        data: { object: {} },
        created: Math.floor(Date.now() / 1000),
        livemode: false,
      };

      const existingEvent = createMockStripeEvent({
        stripeEventId: 'evt_already_processed',
        processed: true,
      });

      mockEventRepository.findOne.mockResolvedValue(existingEvent);

      const result = await service.processWebhook(payload);

      expect(result.success).toBe(true);
      expect(result.message).toBe('Event already processed');
    });

    it('should retry processing for failed event', async () => {
      const payload: StripeWebhookPayload = {
        id: 'evt_failed',
        type: 'customer.subscription.created',
        data: {
          object: {
            id: 'sub_retry',
            customer: 'cus_123',
            status: 'active',
            current_period_start: Math.floor(Date.now() / 1000),
            current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
          },
        },
        created: Math.floor(Date.now() / 1000),
        livemode: false,
      };

      const failedEvent = createMockStripeEvent({
        stripeEventId: 'evt_failed',
        processed: false,
        retryCount: 1,
        data: payload.data,
      });
      const mockSubscription = createMockSubscription();

      mockEventRepository.findOne.mockResolvedValue(failedEvent);
      mockEventRepository.save.mockResolvedValue(failedEvent);
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSubscription);
      mockSubscriptionRepository.save.mockResolvedValue(mockSubscription);

      const result = await service.processWebhook(payload);

      expect(result.success).toBe(true);
      expect(result.message).toBe('Event processed on retry');
    });

    it('should handle processing errors gracefully', async () => {
      const payload: StripeWebhookPayload = {
        id: 'evt_error',
        type: 'customer.subscription.created',
        data: {
          object: {
            id: 'sub_error',
            customer: 'cus_123',
            status: 'active',
          },
        },
        created: Math.floor(Date.now() / 1000),
        livemode: false,
      };

      const mockEvent = createMockStripeEvent({ stripeEventId: 'evt_error' });

      mockEventRepository.findOne.mockResolvedValue(null);
      mockEventRepository.create.mockReturnValue(mockEvent);
      mockEventRepository.save.mockResolvedValue(mockEvent);
      mockSubscriptionRepository.findOne.mockRejectedValue(new Error('Database error'));

      const result = await service.processWebhook(payload);

      expect(result.success).toBe(false);
      expect(result.error).toBe('Database error');
    });
  });

  describe('handleSubscriptionCreated', () => {
    it('should create/link subscription for existing customer', async () => {
      const payload: StripeWebhookPayload = {
        id: 'evt_sub_created',
        type: 'customer.subscription.created',
        data: {
          object: {
            id: 'sub_new_123',
            customer: 'cus_existing',
            status: 'active',
            current_period_start: Math.floor(Date.now() / 1000),
            current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
            trial_end: null,
          },
        },
        created: Math.floor(Date.now() / 1000),
        livemode: false,
      };

      const mockEvent = createMockStripeEvent();
      const mockSubscription = createMockSubscription({ stripeCustomerId: 'cus_existing' });

      mockEventRepository.findOne.mockResolvedValue(null);
      mockEventRepository.create.mockReturnValue(mockEvent);
      mockEventRepository.save.mockResolvedValue(mockEvent);
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSubscription);
      mockSubscriptionRepository.save.mockResolvedValue(mockSubscription);

      const result = await service.processWebhook(payload);

      expect(result.success).toBe(true);
      expect(mockSubscriptionRepository.save).toHaveBeenCalled();
    });
  });

  describe('handleSubscriptionUpdated', () => {
    it('should update subscription status', async () => {
      const payload: StripeWebhookPayload = {
        id: 'evt_sub_updated',
        type: 'customer.subscription.updated',
        data: {
          object: {
            id: 'sub_123',
            customer: 'cus_123',
            status: 'past_due',
            current_period_start: Math.floor(Date.now() / 1000),
            current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
            cancel_at_period_end: false,
            canceled_at: null,
          },
        },
        created: Math.floor(Date.now() / 1000),
        livemode: false,
      };

      const mockEvent = createMockStripeEvent();
      const mockSubscription = createMockSubscription({ stripeSubscriptionId: 'sub_123' });

      mockEventRepository.findOne.mockResolvedValue(null);
      mockEventRepository.create.mockReturnValue(mockEvent);
      mockEventRepository.save.mockResolvedValue(mockEvent);
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSubscription);
      mockSubscriptionRepository.save.mockResolvedValue({ ...mockSubscription, status: 'past_due' });

      const result = await service.processWebhook(payload);

      expect(result.success).toBe(true);
    });

    it('should handle cancel_at_period_end flag', async () => {
      const payload: StripeWebhookPayload = {
        id: 'evt_sub_cancel_scheduled',
        type: 'customer.subscription.updated',
        data: {
          object: {
            id: 'sub_cancel',
            customer: 'cus_123',
            status: 'active',
            current_period_start: Math.floor(Date.now() / 1000),
            current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
            cancel_at_period_end: true,
            canceled_at: null,
          },
        },
        created: Math.floor(Date.now() / 1000),
        livemode: false,
      };

      const mockEvent = createMockStripeEvent({ eventType: 'customer.subscription.updated' });
      const mockSubscription = createMockSubscription({ stripeSubscriptionId: 'sub_cancel' });

      mockEventRepository.findOne.mockResolvedValue(null);
      mockEventRepository.create.mockReturnValue(mockEvent);
      mockEventRepository.save.mockResolvedValue(mockEvent);
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSubscription);
      mockSubscriptionRepository.save.mockImplementation((sub) => Promise.resolve(sub));

      await service.processWebhook(payload);

      expect(mockSubscriptionRepository.save).toHaveBeenCalledWith(
        expect.objectContaining({ cancelAtPeriodEnd: true })
      );
    });
  });

  describe('handleSubscriptionDeleted', () => {
    it('should mark subscription as cancelled', async () => {
      const payload: StripeWebhookPayload = {
        id: 'evt_sub_deleted',
        type: 'customer.subscription.deleted',
        data: {
          object: {
            id: 'sub_deleted',
            customer: 'cus_123',
            status: 'canceled',
          },
        },
        created: Math.floor(Date.now() / 1000),
        livemode: false,
      };

      const mockEvent = createMockStripeEvent();
      const mockSubscription = createMockSubscription({ stripeSubscriptionId: 'sub_deleted' });

      mockEventRepository.findOne.mockResolvedValue(null);
      mockEventRepository.create.mockReturnValue(mockEvent);
      mockEventRepository.save.mockResolvedValue(mockEvent);
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSubscription);
      mockSubscriptionRepository.save.mockImplementation((sub) => Promise.resolve(sub));

      await service.processWebhook(payload);

      expect(mockSubscriptionRepository.save).toHaveBeenCalledWith(
        expect.objectContaining({ status: 'cancelled' })
      );
    });
  });

  describe('handlePaymentSucceeded', () => {
    it('should update subscription with payment info', async () => {
      const payload: StripeWebhookPayload = {
        id: 'evt_payment_success',
        type: 'invoice.payment_succeeded',
        data: {
          object: {
            id: 'inv_123',
            customer: 'cus_123',
            amount_paid: 49900, // $499.00 in cents
            subscription: 'sub_123',
          },
        },
        created: Math.floor(Date.now() / 1000),
        livemode: false,
      };

      const mockEvent = createMockStripeEvent({ eventType: 'invoice.payment_succeeded' });
      const mockSubscription = createMockSubscription({ stripeCustomerId: 'cus_123' });

      mockEventRepository.findOne.mockResolvedValue(null);
      mockEventRepository.create.mockReturnValue(mockEvent);
      mockEventRepository.save.mockResolvedValue(mockEvent);
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSubscription);
      mockSubscriptionRepository.save.mockImplementation((sub) => Promise.resolve(sub));

      await service.processWebhook(payload);

      expect(mockSubscriptionRepository.save).toHaveBeenCalledWith(
        expect.objectContaining({
          status: 'active',
          lastPaymentAmount: 499, // Converted from cents
        })
      );
    });
  });

  describe('handlePaymentFailed', () => {
    it('should mark subscription as past_due', async () => {
      const payload: StripeWebhookPayload = {
        id: 'evt_payment_failed',
        type: 'invoice.payment_failed',
        data: {
          object: {
            id: 'inv_failed',
            customer: 'cus_123',
            attempt_count: 1,
          },
        },
        created: Math.floor(Date.now() / 1000),
        livemode: false,
      };

      const mockEvent = createMockStripeEvent({ eventType: 'invoice.payment_failed' });
      const mockSubscription = createMockSubscription({ stripeCustomerId: 'cus_123', status: 'active' });

      mockEventRepository.findOne.mockResolvedValue(null);
      mockEventRepository.create.mockReturnValue(mockEvent);
      mockEventRepository.save.mockResolvedValue(mockEvent);
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSubscription);
      mockSubscriptionRepository.save.mockImplementation((sub) => Promise.resolve(sub));

      await service.processWebhook(payload);

      expect(mockSubscriptionRepository.save).toHaveBeenCalledWith(
        expect.objectContaining({ status: 'past_due' })
      );
    });
  });

  describe('handleCheckoutCompleted', () => {
    it('should link Stripe customer to tenant', async () => {
      const payload: StripeWebhookPayload = {
        id: 'evt_checkout_completed',
        type: 'checkout.session.completed',
        data: {
          object: {
            id: 'cs_123',
            customer: 'cus_new',
            subscription: 'sub_new',
            metadata: {
              tenant_id: 'tenant-uuid-1',
            },
          },
        },
        created: Math.floor(Date.now() / 1000),
        livemode: false,
      };

      const mockEvent = createMockStripeEvent({ eventType: 'checkout.session.completed' });
      const mockSubscription = createMockSubscription({ tenantId: 'tenant-uuid-1' });

      mockEventRepository.findOne.mockResolvedValue(null);
      mockEventRepository.create.mockReturnValue(mockEvent);
      mockEventRepository.save.mockResolvedValue(mockEvent);
      mockSubscriptionRepository.findOne.mockResolvedValue(mockSubscription);
      mockSubscriptionRepository.save.mockImplementation((sub) => Promise.resolve(sub));

      await service.processWebhook(payload);

      expect(mockSubscriptionRepository.save).toHaveBeenCalledWith(
        expect.objectContaining({
          stripeCustomerId: 'cus_new',
          stripeSubscriptionId: 'sub_new',
          status: 'active',
        })
      );
    });
  });

  describe('retryProcessing', () => {
    it('should retry and succeed', async () => {
      const failedEvent = createMockStripeEvent({
        processed: false,
        retryCount: 2,
        data: {
          object: {
            id: 'sub_retry',
            customer: 'cus_123',
            status: 'active',
            current_period_start: Math.floor(Date.now() / 1000),
            current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
          },
        },
      });
      const mockSubscription = createMockSubscription();

      mockSubscriptionRepository.findOne.mockResolvedValue(mockSubscription);
      mockSubscriptionRepository.save.mockResolvedValue(mockSubscription);
      mockEventRepository.save.mockResolvedValue(failedEvent);

      const result = await service.retryProcessing(failedEvent as any);

      expect(result.success).toBe(true);
      expect(result.message).toBe('Event processed on retry');
    });

    it('should fail if max retries exceeded', async () => {
      const maxRetriedEvent = createMockStripeEvent({
        processed: false,
        retryCount: 5,
        errorMessage: 'Previous error',
      });

      const result = await service.retryProcessing(maxRetriedEvent as any);

      expect(result.success).toBe(false);
      expect(result.message).toBe('Max retries exceeded');
    });
  });

  describe('getFailedEvents', () => {
    it('should return unprocessed events', async () => {
      const failedEvents = [
        createMockStripeEvent({ processed: false }),
        createMockStripeEvent({ processed: false, stripeEventId: 'evt_2' }),
      ];

      mockEventRepository.find.mockResolvedValue(failedEvents);

      const result = await service.getFailedEvents();

      expect(result).toHaveLength(2);
      expect(mockEventRepository.find).toHaveBeenCalledWith({
        where: { processed: false },
        order: { createdAt: 'ASC' },
        take: 100,
      });
    });

    it('should respect limit parameter', async () => {
      mockEventRepository.find.mockResolvedValue([]);

      await service.getFailedEvents(50);

      expect(mockEventRepository.find).toHaveBeenCalledWith(
        expect.objectContaining({ take: 50 })
      );
    });
  });

  describe('findByStripeEventId', () => {
    it('should find event by Stripe ID', async () => {
      const mockEvent = createMockStripeEvent({ stripeEventId: 'evt_find' });
      mockEventRepository.findOne.mockResolvedValue(mockEvent);

      const result = await service.findByStripeEventId('evt_find');

      expect(result).toBeDefined();
      expect(result?.stripeEventId).toBe('evt_find');
    });

    it('should return null if not found', async () => {
      mockEventRepository.findOne.mockResolvedValue(null);

      const result = await service.findByStripeEventId('evt_notfound');

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

  describe('getRecentEvents', () => {
    it('should return recent events with default options', async () => {
      const mockEvents = [createMockStripeEvent()];
      const mockQueryBuilder = {
        andWhere: jest.fn().mockReturnThis(),
        orderBy: jest.fn().mockReturnThis(),
        take: jest.fn().mockReturnThis(),
        getMany: jest.fn().mockResolvedValue(mockEvents),
      };

      mockEventRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);

      const result = await service.getRecentEvents();

      expect(result).toHaveLength(1);
      expect(mockQueryBuilder.take).toHaveBeenCalledWith(50);
    });

    it('should filter by event type', async () => {
      const mockQueryBuilder = {
        andWhere: jest.fn().mockReturnThis(),
        orderBy: jest.fn().mockReturnThis(),
        take: jest.fn().mockReturnThis(),
        getMany: jest.fn().mockResolvedValue([]),
      };

      mockEventRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);

      await service.getRecentEvents({ eventType: 'invoice.payment_succeeded' });

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'event.eventType = :eventType',
        { eventType: 'invoice.payment_succeeded' }
      );
    });

    it('should filter by processed status', async () => {
      const mockQueryBuilder = {
        andWhere: jest.fn().mockReturnThis(),
        orderBy: jest.fn().mockReturnThis(),
        take: jest.fn().mockReturnThis(),
        getMany: jest.fn().mockResolvedValue([]),
      };

      mockEventRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder as any);

      await service.getRecentEvents({ processed: false });

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'event.processed = :processed',
        { processed: false }
      );
    });
  });
});
