import { describe, it, expect, vi, beforeEach } from 'vitest';
import { TestBed } from '@angular/core/testing';
import { ZodError } from 'zod';
import { ShoppingCartFacade } from './shopping-cart.facade';
import { CheckoutService, ShoppingCartService } from '../services';
import { Customer, Payer as CrmPayer } from '@isa/crm/data-access';
import { ShippingAddressDTO as CrmShippingAddressDTO } from '@generated/swagger/crm-api';
import { Order } from '../models';

describe('ShoppingCartFacade', () => {
  let facade: ShoppingCartFacade;
  let mockCheckoutService: {
    complete: ReturnType<typeof vi.fn>;
  };
  let mockShoppingCartService: {
    createShoppingCart: ReturnType<typeof vi.fn>;
    getShoppingCart: ReturnType<typeof vi.fn>;
    removeItem: ReturnType<typeof vi.fn>;
    updateItem: ReturnType<typeof vi.fn>;
  };

  beforeEach(() => {
    mockCheckoutService = {
      complete: vi.fn(),
    };

    mockShoppingCartService = {
      createShoppingCart: vi.fn(),
      getShoppingCart: vi.fn(),
      removeItem: vi.fn(),
      updateItem: vi.fn(),
    };

    TestBed.configureTestingModule({
      providers: [
        ShoppingCartFacade,
        { provide: CheckoutService, useValue: mockCheckoutService },
        { provide: ShoppingCartService, useValue: mockShoppingCartService },
      ],
    });

    facade = TestBed.inject(ShoppingCartFacade);
  });

  describe('completeWithCrmData', () => {
    it('should transform CRM data and complete order successfully', async () => {
      // Arrange
      const mockOrders: Order[] = [
        { id: 1, orderNumber: 'ORDER-001' } as Order,
      ];

      mockCheckoutService.complete.mockResolvedValue(mockOrders);

      const customer: Customer = {
        id: 123,
        customerNumber: 'CUST-123',
        customerType: 8, // B2C
        firstName: 'John',
        lastName: 'Doe',
        notificationChannels: 1,
        features: [
          { key: 'FEATURE1', value: 'value1' },
          { key: 'FEATURE2', value: 'value2' },
        ],
      } as Customer;

      // Act
      const result = await facade.completeWithCrmData({
        shoppingCartId: 456,
        crmCustomer: customer,
      });

      // Assert
      expect(result).toEqual(mockOrders);
      expect(mockCheckoutService.complete).toHaveBeenCalledOnce();
      expect(mockCheckoutService.complete).toHaveBeenCalledWith(
        expect.objectContaining({
          shoppingCartId: 456,
          buyer: expect.objectContaining({
            reference: { id: 123 },
            source: 123,
            buyerType: 8,
            buyerNumber: 'CUST-123',
            firstName: 'John',
            lastName: 'Doe',
          }),
          notificationChannels: 1,
          customerFeatures: { FEATURE1: 'FEATURE1', FEATURE2: 'FEATURE2' },
        }),
        undefined,
      );
    });

    it('should transform CRM shipping address when provided', async () => {
      // Arrange
      const mockOrders: Order[] = [
        { id: 1, orderNumber: 'ORDER-001' } as Order,
      ];

      mockCheckoutService.complete.mockResolvedValue(mockOrders);

      const customer: Customer = {
        id: 123,
        customerNumber: 'CUST-123',
        customerType: 8,
        firstName: 'John',
        lastName: 'Doe',
        notificationChannels: 1,
        features: [],
      } as Customer;

      const crmShippingAddress: CrmShippingAddressDTO = {
        id: 789,
        firstName: 'Jane',
        lastName: 'Doe',
        address: {
          street: 'Main St',
          streetNumber: '123',
          zipCode: '12345',
          city: 'Berlin',
          country: 'DE',
        } as any,
      };

      // Act
      await facade.completeWithCrmData({
        shoppingCartId: 456,
        crmCustomer: customer,
        crmShippingAddress,
      });

      // Assert
      expect(mockCheckoutService.complete).toHaveBeenCalledWith(
        expect.objectContaining({
          shippingAddress: expect.objectContaining({
            reference: { id: 789 },
            source: 789,
            firstName: 'Jane',
            lastName: 'Doe',
            address: expect.objectContaining({
              street: 'Main St',
              streetNumber: '123',
              zipCode: '12345',
              city: 'Berlin',
              country: 'DE',
            }),
          }),
        }),
        undefined,
      );
    });

    it('should transform CRM payer when provided', async () => {
      // Arrange
      const mockOrders: Order[] = [
        { id: 1, orderNumber: 'ORDER-001' } as Order,
      ];

      mockCheckoutService.complete.mockResolvedValue(mockOrders);

      const customer: Customer = {
        id: 123,
        customerNumber: 'CUST-123',
        customerType: 8,
        firstName: 'John',
        lastName: 'Doe',
        notificationChannels: 1,
        features: [],
      } as Customer;

      const crmPayer: CrmPayer = {
        id: 999,
        payerNumber: 'PAY-999',
        payerType: 16, // B2B
        payerStatus: 0,
        firstName: 'Billing',
        lastName: 'Company',
      } as CrmPayer;

      // Act
      await facade.completeWithCrmData({
        shoppingCartId: 456,
        crmCustomer: customer,
        crmPayer,
      });

      // Assert
      expect(mockCheckoutService.complete).toHaveBeenCalledWith(
        expect.objectContaining({
          payer: expect.objectContaining({
            reference: { id: 999 },
            source: 999,
            payerType: 16,
            payerNumber: 'PAY-999',
            payerStatus: 0,
            firstName: 'Billing',
            lastName: 'Company',
          }),
        }),
        undefined,
      );
    });

    it('should use provided notificationChannels over customer default', async () => {
      // Arrange
      mockCheckoutService.complete.mockResolvedValue([]);

      const customer: Customer = {
        id: 123,
        customerNumber: 'CUST-123',
        customerType: 8,
        firstName: 'John',
        lastName: 'Doe',
        notificationChannels: 1, // Customer default
        features: [],
      } as Customer;

      // Act
      await facade.completeWithCrmData({
        shoppingCartId: 456,
        crmCustomer: customer,
        notificationChannels: 4, // Override
      });

      // Assert
      expect(mockCheckoutService.complete).toHaveBeenCalledWith(
        expect.objectContaining({
          notificationChannels: 4, // Should use override
        }),
        undefined,
      );
    });

    it('should use customer notificationChannels when not provided', async () => {
      // Arrange
      mockCheckoutService.complete.mockResolvedValue([]);

      const customer: Customer = {
        id: 123,
        customerNumber: 'CUST-123',
        customerType: 8,
        firstName: 'John',
        lastName: 'Doe',
        notificationChannels: 2, // Customer default
        features: [],
      } as Customer;

      // Act
      await facade.completeWithCrmData({
        shoppingCartId: 456,
        crmCustomer: customer,
      });

      // Assert
      expect(mockCheckoutService.complete).toHaveBeenCalledWith(
        expect.objectContaining({
          notificationChannels: 2, // Should use customer default
        }),
        undefined,
      );
    });

    it('should default to 1 when customer notificationChannels is undefined', async () => {
      // Arrange
      mockCheckoutService.complete.mockResolvedValue([]);

      const customer: Customer = {
        id: 123,
        customerNumber: 'CUST-123',
        customerType: 8,
        firstName: 'John',
        lastName: 'Doe',
        notificationChannels: undefined, // Not set
        features: [],
      } as Customer;

      // Act
      await facade.completeWithCrmData({
        shoppingCartId: 456,
        crmCustomer: customer,
      });

      // Assert
      expect(mockCheckoutService.complete).toHaveBeenCalledWith(
        expect.objectContaining({
          notificationChannels: 1, // Should default to 1
        }),
        undefined,
      );
    });

    it('should handle missing optional shipping address', async () => {
      // Arrange
      mockCheckoutService.complete.mockResolvedValue([]);

      const customer: Customer = {
        id: 123,
        customerNumber: 'CUST-123',
        customerType: 8,
        firstName: 'John',
        lastName: 'Doe',
        notificationChannels: 1,
        features: [],
      } as Customer;

      // Act
      await facade.completeWithCrmData({
        shoppingCartId: 456,
        crmCustomer: customer,
        crmShippingAddress: undefined,
      });

      // Assert
      expect(mockCheckoutService.complete).toHaveBeenCalledWith(
        expect.objectContaining({
          shippingAddress: undefined,
        }),
        undefined,
      );
    });

    it('should handle missing optional payer', async () => {
      // Arrange
      mockCheckoutService.complete.mockResolvedValue([]);

      const customer: Customer = {
        id: 123,
        customerNumber: 'CUST-123',
        customerType: 8,
        firstName: 'John',
        lastName: 'Doe',
        notificationChannels: 1,
        features: [],
      } as Customer;

      // Act
      await facade.completeWithCrmData({
        shoppingCartId: 456,
        crmCustomer: customer,
        crmPayer: undefined,
      });

      // Assert
      expect(mockCheckoutService.complete).toHaveBeenCalledWith(
        expect.objectContaining({
          payer: undefined,
        }),
        undefined,
      );
    });

    it('should pass abort signal to underlying service', async () => {
      // Arrange
      mockCheckoutService.complete.mockResolvedValue([]);

      const customer: Customer = {
        id: 123,
        customerNumber: 'CUST-123',
        customerType: 8,
        firstName: 'John',
        lastName: 'Doe',
        notificationChannels: 1,
        features: [],
      } as Customer;

      const abortController = new AbortController();

      // Act
      await facade.completeWithCrmData(
        {
          shoppingCartId: 456,
          crmCustomer: customer,
        },
        abortController.signal,
      );

      // Assert
      expect(mockCheckoutService.complete).toHaveBeenCalledWith(
        expect.any(Object),
        abortController.signal,
      );
    });

    it('should extract customer features correctly', async () => {
      // Arrange
      mockCheckoutService.complete.mockResolvedValue([]);

      const customer: Customer = {
        id: 123,
        customerNumber: 'CUST-123',
        customerType: 8,
        firstName: 'John',
        lastName: 'Doe',
        notificationChannels: 1,
        features: [
          { key: 'FEATURE_A', value: 'some value' },
          { key: 'FEATURE_B', value: 'another value' },
          { key: 'FEATURE_C', value: 'third value' },
        ],
      } as Customer;

      // Act
      await facade.completeWithCrmData({
        shoppingCartId: 456,
        crmCustomer: customer,
      });

      // Assert
      expect(mockCheckoutService.complete).toHaveBeenCalledWith(
        expect.objectContaining({
          customerFeatures: {
            FEATURE_A: 'FEATURE_A',
            FEATURE_B: 'FEATURE_B',
            FEATURE_C: 'FEATURE_C',
          },
        }),
        undefined,
      );
    });

    it('should handle empty customer features', async () => {
      // Arrange
      mockCheckoutService.complete.mockResolvedValue([]);

      const customer: Customer = {
        id: 123,
        customerNumber: 'CUST-123',
        customerType: 8,
        firstName: 'John',
        lastName: 'Doe',
        notificationChannels: 1,
        features: [],
      } as Customer;

      // Act
      await facade.completeWithCrmData({
        shoppingCartId: 456,
        crmCustomer: customer,
      });

      // Assert
      expect(mockCheckoutService.complete).toHaveBeenCalledWith(
        expect.objectContaining({
          customerFeatures: {},
        }),
        undefined,
      );
    });

    it('should throw error for invalid shopping cart ID', async () => {
      // Arrange
      const customer: Customer = {
        id: 123,
        customerNumber: 'CUST-123',
        customerType: 8,
        firstName: 'John',
        lastName: 'Doe',
        notificationChannels: 1,
        features: [],
      } as Customer;

      // Act & Assert
      try {
        await facade.completeWithCrmData({
          shoppingCartId: -1, // Invalid ID
          crmCustomer: customer,
        });
        expect.fail('Should have thrown ZodError');
      } catch (error) {
        expect(error).toBeInstanceOf(ZodError);
      }
    });

    it('should throw error for invalid customer data', async () => {
      // Act & Assert
      try {
        await facade.completeWithCrmData({
          shoppingCartId: 456,
          customer: { invalid: 'data' } as any, // Invalid customer
        });
        expect.fail('Should have thrown ZodError');
      } catch (error) {
        expect(error).toBeInstanceOf(ZodError);
      }
    });

    it('should propagate errors from checkout service', async () => {
      // Arrange
      const checkoutError = new Error('Checkout failed');
      mockCheckoutService.complete.mockRejectedValue(checkoutError);

      const customer: Customer = {
        id: 123,
        customerNumber: 'CUST-123',
        customerType: 8,
        firstName: 'John',
        lastName: 'Doe',
        notificationChannels: 1,
        features: [],
      } as Customer;

      // Act & Assert
      await expect(
        facade.completeWithCrmData({
          shoppingCartId: 456,
          crmCustomer: customer,
        }),
      ).rejects.toThrow('Checkout failed');
    });

    it('should handle all parameters together', async () => {
      // Arrange
      const mockOrders: Order[] = [
        { id: 1, orderNumber: 'ORDER-001' } as Order,
        { id: 2, orderNumber: 'ORDER-002' } as Order,
      ];

      mockCheckoutService.complete.mockResolvedValue(mockOrders);

      const customer: Customer = {
        id: 123,
        customerNumber: 'CUST-123',
        customerType: 16, // B2B
        firstName: 'John',
        lastName: 'Doe',
        gender: 2,
        title: 'Mr.',
        dateOfBirth: '1980-01-15',
        notificationChannels: 4,
        features: [{ key: 'B2B_CUSTOMER', value: 'true' }],
        communicationDetails: {
          email: 'john.doe@example.com',
          phone: '+49 123 456789',
        },
        organisation: {
          name: 'ACME Corp',
        },
        address: {
          street: 'Main St',
          streetNumber: '42',
          zipCode: '12345',
          city: 'Berlin',
          country: 'DE',
        },
      } as Customer;

      const crmShippingAddress: CrmShippingAddressDTO = {
        id: 789,
        firstName: 'Jane',
        lastName: 'Doe',
        address: {
          street: 'Delivery St',
          streetNumber: '99',
          zipCode: '54321',
          city: 'Hamburg',
          country: 'DE',
        },
      };

      const crmPayer: CrmPayer = {
        id: 999,
        payerNumber: 'PAY-999',
        payerType: 16,
        payerStatus: 0,
        firstName: 'Billing',
        lastName: 'Department',
        address: {
          street: 'Billing St',
          streetNumber: '1',
          zipCode: '11111',
          city: 'Munich',
          country: 'DE',
        },
      } as CrmPayer;

      const abortController = new AbortController();

      // Act
      const result = await facade.completeWithCrmData(
        {
          shoppingCartId: 456,
          crmCustomer: customer,
          crmShippingAddress,
          crmPayer,
          notificationChannels: 8,
        },
        abortController.signal,
      );

      // Assert
      expect(result).toEqual(mockOrders);
      expect(mockCheckoutService.complete).toHaveBeenCalledWith(
        expect.objectContaining({
          shoppingCartId: 456,
          buyer: expect.objectContaining({
            reference: { id: 123 },
            source: 123,
            buyerType: 16,
            buyerNumber: 'CUST-123',
            firstName: 'John',
            lastName: 'Doe',
          }),
          shippingAddress: expect.objectContaining({
            reference: { id: 789 },
            source: 789,
            firstName: 'Jane',
            lastName: 'Doe',
          }),
          payer: expect.objectContaining({
            reference: { id: 999 },
            source: 999,
            payerType: 16,
            payerNumber: 'PAY-999',
          }),
          notificationChannels: 8,
          customerFeatures: { B2B_CUSTOMER: 'B2B_CUSTOMER' },
        }),
        abortController.signal,
      );
    });
  });

  describe('complete', () => {
    it('should delegate to checkout service', async () => {
      // Arrange
      const mockOrders: Order[] = [
        { id: 1, orderNumber: 'ORDER-001' } as Order,
      ];

      mockCheckoutService.complete.mockResolvedValue(mockOrders);

      const params = {
        shoppingCartId: 123,
        buyer: { buyerType: 8 } as any,
        notificationChannels: 1 as any,
        customerFeatures: {},
      };

      // Act
      const result = await facade.complete(params);

      // Assert
      expect(result).toEqual(mockOrders);
      expect(mockCheckoutService.complete).toHaveBeenCalledWith(
        params,
        undefined,
      );
    });
  });
});
