import { TestBed } from '@angular/core/testing';
import { of, throwError } from 'rxjs';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { AvailabilityService } from './availability.service';
import {
  AvailabilityService as GeneratedAvailabilityService,
  AvailabilityDTO,
} from '@generated/swagger/availability-api';
import { LogisticianService as GeneratedLogisticianService } from '@generated/swagger/oms-api';
import { ResponseArgsError } from '@isa/common/data-access';
import { BranchService, RemissionStockService } from '@isa/remission/data-access';
import { SupplierService } from '@isa/checkout/data-access';
import { LoggingService } from '@isa/core/logging';

describe('AvailabilityService', () => {
  let service: AvailabilityService;
  let mockAvailabilityService: any;
  let mockLogisticianService: any;
  let mockBranchService: any;
  let mockStockService: any;
  let mockSupplierService: any;
  let mockLoggingService: any;

  const mockAvailabilityDTO: AvailabilityDTO = {
    itemId: 123,
    status: 1024,
    preferred: 1,
    ssc: '10',
    sscText: 'Available',
    qty: 5,
    price: {
      value: { value: 19.99, currency: 'EUR', currencySymbol: '€' },
      vat: { value: 3.8, inPercent: 19, label: '19%', vatType: 1 },
    },
    priceMaintained: true,
    at: '2025-10-15',
    altAt: '2025-10-20',
    requestStatusCode: '0',
  };

  beforeEach(() => {
    mockAvailabilityService = {
      AvailabilityStoreAvailability: vi.fn(),
      AvailabilityShippingAvailability: vi.fn(),
    };

    mockLogisticianService = {
      LogisticianGetLogisticians: vi.fn(),
    };

    mockBranchService = {
      getDefaultBranch: vi.fn(),
    };

    mockStockService = {
      fetchStock: vi.fn(),
      fetchStockInfos: vi.fn(),
    };

    mockSupplierService = {
      getTakeAwaySupplier: vi.fn(),
    };

    mockLoggingService = {
      log: vi.fn(),
      info: vi.fn(),
      warn: vi.fn(),
      error: vi.fn(),
      debug: vi.fn(),
    };

    TestBed.configureTestingModule({
      providers: [
        AvailabilityService,
        { provide: GeneratedAvailabilityService, useValue: mockAvailabilityService },
        { provide: GeneratedLogisticianService, useValue: mockLogisticianService },
        { provide: BranchService, useValue: mockBranchService },
        { provide: RemissionStockService, useValue: mockStockService },
        { provide: SupplierService, useValue: mockSupplierService },
        { provide: LoggingService, useValue: mockLoggingService },
      ],
    });

    service = TestBed.inject(AvailabilityService);
  });

  describe('getAvailabilities', () => {
    it('should be created', () => {
      expect(service).toBeTruthy();
    });

    it('should throw error for invalid params', async () => {
      await expect(service.getAvailabilities({})).rejects.toThrow();
    });

    it('should throw error for unsupported order type', async () => {
      await expect(
        service.getAvailabilities({
          orderType: 'InvalidType',
          items: [{ itemId: 123, ean: '1234567890', quantity: 1 }],
        }),
      ).rejects.toThrow();
    });
  });

  describe('InStore availability (Rücklage)', () => {
    it('should fetch in-store availability with branch context', async () => {
      const mockStock = { id: 'stock-123', name: 'Test Stock' };
      const mockStockInfo = {
        itemId: 123,
        inStock: 5,
        retailPrice: { value: { value: 19.99 } },
      };
      const mockSupplier = { id: 1, name: 'Supplier F' };

      mockStockService.fetchStock.mockResolvedValue(mockStock);
      mockStockService.fetchStockInfos.mockResolvedValue([mockStockInfo]);
      mockSupplierService.getTakeAwaySupplier.mockResolvedValue(mockSupplier);

      const result = await service.getAvailabilities({
        orderType: 'Rücklage',
        branchId: 42,
        itemsIds: [123],
      });

      expect(mockStockService.fetchStock).toHaveBeenCalledWith(42, undefined);
      expect(mockStockService.fetchStockInfos).toHaveBeenCalledWith(
        { itemIds: [123], stockId: 'stock-123' },
        undefined,
      );
      expect(mockSupplierService.getTakeAwaySupplier).toHaveBeenCalledWith(undefined);

      expect(result).toHaveProperty('123');
      expect(result['123'].itemId).toBe(123);
      expect(result['123'].qty).toBe(5);
    });

    it('should return empty when branch has no stock ID', async () => {
      const mockStock = { name: 'Test Stock' }; // No id property

      mockStockService.fetchStock.mockResolvedValue(mockStock);

      const result = await service.getAvailabilities({
        orderType: 'Rücklage',
        branchId: 42,
        itemsIds: [123],
      });

      expect(result).toEqual({});
      expect(mockStockService.fetchStockInfos).not.toHaveBeenCalled();
    });
  });

  describe('Pickup availability (Abholung)', () => {
    it('should fetch pickup availability with branch context', async () => {
      const mockResponse = {
        error: null,
        result: [mockAvailabilityDTO],
        message: null,
        invalidProperties: null,
      };

      mockAvailabilityService.AvailabilityStoreAvailability.mockReturnValue(
        of(mockResponse),
      );

      const result = await service.getAvailabilities({
        orderType: 'Abholung',
        branchId: 42,
        items: [{ itemId: 123, ean: '1234567890', quantity: 1 }],
      });

      expect(mockAvailabilityService.AvailabilityStoreAvailability).toHaveBeenCalled();
      expect(result).toEqual({ '123': mockAvailabilityDTO });
    });
  });

  describe('Delivery availability (Versand)', () => {
    it('should fetch standard delivery availability', async () => {
      const mockResponse = {
        error: null,
        result: [mockAvailabilityDTO],
        message: null,
        invalidProperties: null,
      };

      mockAvailabilityService.AvailabilityShippingAvailability.mockReturnValue(
        of(mockResponse),
      );

      const result = await service.getAvailabilities({
        orderType: 'Versand',
        items: [{ itemId: 123, ean: '1234567890', quantity: 3 }],
      });

      expect(mockAvailabilityService.AvailabilityShippingAvailability).toHaveBeenCalledWith(
        expect.arrayContaining([
          expect.objectContaining({
            ean: '1234567890',
            itemId: '123',
            qty: 3,
          }),
        ]),
      );

      expect(result).toEqual({ '123': mockAvailabilityDTO });
    });

    it('should not include shopId for delivery', async () => {
      const mockResponse = {
        error: null,
        result: [mockAvailabilityDTO],
        message: null,
        invalidProperties: null,
      };

      mockAvailabilityService.AvailabilityShippingAvailability.mockReturnValue(
        of(mockResponse),
      );

      await service.getAvailabilities({
        orderType: 'Versand',
        items: [{ itemId: 123, ean: '1234567890', quantity: 1 }],
      });

      const callArgs = mockAvailabilityService.AvailabilityShippingAvailability.mock.calls[0][0];
      expect(callArgs[0]).not.toHaveProperty('shopId');
    });
  });

  describe('DIG delivery availability (DIG-Versand)', () => {
    it('should fetch DIG delivery availability', async () => {
      const mockResponse = {
        error: null,
        result: [mockAvailabilityDTO],
        message: null,
        invalidProperties: null,
      };

      mockAvailabilityService.AvailabilityShippingAvailability.mockReturnValue(
        of(mockResponse),
      );

      const result = await service.getAvailabilities({
        orderType: 'DIG-Versand',
        items: [{ itemId: 123, ean: '1234567890', quantity: 2 }],
      });

      expect(mockAvailabilityService.AvailabilityShippingAvailability).toHaveBeenCalled();
      expect(result).toEqual({ '123': mockAvailabilityDTO });
    });
  });

  describe('B2B delivery availability (B2B-Versand)', () => {
    const mockLogistician = {
      id: 5,
      logisticianNumber: '2470',
      name: 'DHL',
    };

    const mockBranch = {
      id: 42,
      name: 'Test Branch',
      branchNumber: '001',
    };

    beforeEach(() => {
      mockLogisticianService.LogisticianGetLogisticians.mockReturnValue(
        of({
          error: null,
          result: [mockLogistician],
          message: null,
          invalidProperties: null,
        }),
      );

      mockBranchService.getDefaultBranch.mockResolvedValue(mockBranch);
    });

    it('should fetch B2B availability with logistician override and default branch', async () => {
      const mockResponse = {
        error: null,
        result: [{ ...mockAvailabilityDTO, logisticianId: 99 }],
        message: null,
        invalidProperties: null,
      };

      mockAvailabilityService.AvailabilityStoreAvailability.mockReturnValue(
        of(mockResponse),
      );

      const result = await service.getAvailabilities({
        orderType: 'B2B-Versand',
        items: [{ itemId: 123, ean: '1234567890', quantity: 1 }],
      });

      expect(mockBranchService.getDefaultBranch).toHaveBeenCalled();
      expect(mockLogisticianService.LogisticianGetLogisticians).toHaveBeenCalled();
      expect(mockAvailabilityService.AvailabilityStoreAvailability).toHaveBeenCalled();

      // Verify logistician was overridden to 2470's ID
      expect(result['123'].logisticianId).toBe(5);
    });

    it('should use store endpoint for B2B', async () => {
      const mockResponse = {
        error: null,
        result: [mockAvailabilityDTO],
        message: null,
        invalidProperties: null,
      };

      mockAvailabilityService.AvailabilityStoreAvailability.mockReturnValue(
        of(mockResponse),
      );

      await service.getAvailabilities({
        orderType: 'B2B-Versand',
        items: [{ itemId: 123, ean: '1234567890', quantity: 1 }],
      });

      expect(mockAvailabilityService.AvailabilityStoreAvailability).toHaveBeenCalled();
      expect(mockAvailabilityService.AvailabilityShippingAvailability).not.toHaveBeenCalled();
    });

    it('should throw error if logistician 2470 not found', async () => {
      mockLogisticianService.LogisticianGetLogisticians.mockReturnValue(
        of({
          error: null,
          result: [{ id: 1, logisticianNumber: '1234', name: 'Other' }],
          message: null,
          invalidProperties: null,
        }),
      );

      await expect(
        service.getAvailabilities({
          orderType: 'B2B-Versand',
          items: [{ itemId: 123, ean: '1234567890', quantity: 1 }],
        }),
      ).rejects.toThrow('Logistician 2470 not found');
    });

    it('should throw error if default branch has no ID', async () => {
      mockBranchService.getDefaultBranch.mockResolvedValue({ name: 'Test' });

      await expect(
        service.getAvailabilities({
          orderType: 'B2B-Versand',
          items: [{ itemId: 123, ean: '1234567890', quantity: 1 }],
        }),
      ).rejects.toThrow('Default branch has no ID');
    });
  });

  describe('Download availability', () => {
    it('should fetch download availability with quantity 1', async () => {
      const mockResponse = {
        error: null,
        result: [mockAvailabilityDTO],
        message: null,
        invalidProperties: null,
      };

      mockAvailabilityService.AvailabilityShippingAvailability.mockReturnValue(
        of(mockResponse),
      );

      const result = await service.getAvailabilities({
        orderType: 'Download',
        items: [{ itemId: 123, ean: '1234567890' }],
      });

      const callArgs = mockAvailabilityService.AvailabilityShippingAvailability.mock.calls[0][0];
      expect(callArgs[0].qty).toBe(1); // Always 1 for downloads
      expect(result).toEqual({ '123': mockAvailabilityDTO });
    });

    it('should validate download availability (supplier 16 with 0 stock)', async () => {
      const unavailableDownload: AvailabilityDTO = {
        ...mockAvailabilityDTO,
        supplierId: 16,
        qty: 0,
        preferred: 1,
      };

      const mockResponse = {
        error: null,
        result: [unavailableDownload],
        message: null,
        invalidProperties: null,
      };

      mockAvailabilityService.AvailabilityShippingAvailability.mockReturnValue(
        of(mockResponse),
      );

      const result = await service.getAvailabilities({
        orderType: 'Download',
        items: [{ itemId: 123, ean: '1234567890' }],
      });

      expect(result).toEqual({}); // Should be empty due to validation
    });

    it('should validate download availability (invalid status code)', async () => {
      const invalidStatusDownload: AvailabilityDTO = {
        ...mockAvailabilityDTO,
        status: 512 as any, // Invalid code for downloads (valid AvailabilityType but not for downloads)
        preferred: 1,
      };

      const mockResponse = {
        error: null,
        result: [invalidStatusDownload],
        message: null,
        invalidProperties: null,
      };

      mockAvailabilityService.AvailabilityShippingAvailability.mockReturnValue(
        of(mockResponse),
      );

      const result = await service.getAvailabilities({
        orderType: 'Download',
        items: [{ itemId: 123, ean: '1234567890' }],
      });

      expect(result).toEqual({}); // Should be empty due to validation
    });

    it('should accept valid download availability codes', async () => {
      const validCodes: Array<2 | 32 | 256 | 1024 | 2048 | 4096> = [2, 32, 256, 1024, 2048, 4096];

      for (const code of validCodes) {
        const validDownload: AvailabilityDTO = {
          ...mockAvailabilityDTO,
          status: code,
          preferred: 1,
        };

        const mockResponse = {
          error: null,
          result: [validDownload],
          message: null,
          invalidProperties: null,
        };

        mockAvailabilityService.AvailabilityShippingAvailability.mockReturnValue(
          of(mockResponse),
        );

        const result = await service.getAvailabilities({
          orderType: 'Download',
          items: [{ itemId: 123, ean: '1234567890' }],
        });

        expect(result).toHaveProperty('123');
      }
    });
  });

  describe('Abort signal support', () => {
    it('should support abort signal cancellation', async () => {
      const abortController = new AbortController();
      const mockError = new Error('Request aborted');

      mockAvailabilityService.AvailabilityShippingAvailability.mockReturnValue(
        throwError(() => mockError),
      );

      await expect(
        service.getAvailabilities(
          {
            orderType: 'Versand',
            items: [{ itemId: 123, ean: '1234567890', quantity: 1 }],
          },
          abortController.signal,
        ),
      ).rejects.toThrow();
    });
  });

  describe('Multiple items', () => {
    it('should handle multiple items in single request', async () => {
      const mockResponse = {
        error: null,
        result: [
          { ...mockAvailabilityDTO, itemId: 123, preferred: 1 },
          { ...mockAvailabilityDTO, itemId: 456, preferred: 1 },
        ],
        message: null,
        invalidProperties: null,
      };

      mockAvailabilityService.AvailabilityShippingAvailability.mockReturnValue(
        of(mockResponse),
      );

      const result = await service.getAvailabilities({
        orderType: 'Versand',
        items: [
          { itemId: 123, ean: '1234567890', quantity: 1 },
          { itemId: 456, ean: '0987654321', quantity: 2 },
        ],
      });

      expect(Object.keys(result)).toHaveLength(2);
      expect(result).toHaveProperty('123');
      expect(result).toHaveProperty('456');
    });
  });

  describe('Preferred availability selection', () => {
    it('should select preferred availability when multiple options exist', async () => {
      const nonPreferred: AvailabilityDTO = {
        ...mockAvailabilityDTO,
        preferred: 0,
        price: { value: { value: 25.0 } },
      };

      const preferred: AvailabilityDTO = {
        ...mockAvailabilityDTO,
        preferred: 1,
        price: { value: { value: 19.99 } },
      };

      const mockResponse = {
        error: null,
        result: [nonPreferred, preferred, { ...nonPreferred, preferred: 0 }],
        message: null,
        invalidProperties: null,
      };

      mockAvailabilityService.AvailabilityShippingAvailability.mockReturnValue(
        of(mockResponse),
      );

      const result = await service.getAvailabilities({
        orderType: 'Versand',
        items: [{ itemId: 123, ean: '1234567890', quantity: 1 }],
      });

      expect(result['123'].preferred).toBe(1);
      expect(result['123'].price?.value?.value).toBe(19.99);
    });
  });

  describe('getAvailability', () => {
    it('should fetch availability for a single item (InStore)', async () => {
      const mockStock = { id: 'stock-123', name: 'Test Stock' };
      const mockStockInfo = {
        itemId: 123,
        inStock: 5,
        retailPrice: { value: { value: 19.99 } },
      };
      const mockSupplier = { id: 1, name: 'Supplier F' };

      mockStockService.fetchStock.mockResolvedValue(mockStock);
      mockStockService.fetchStockInfos.mockResolvedValue([mockStockInfo]);
      mockSupplierService.getTakeAwaySupplier.mockResolvedValue(mockSupplier);

      const result = await service.getAvailability({
        orderType: 'Rücklage',
        branchId: 42,
        itemId: 123,
      });

      expect(mockStockService.fetchStock).toHaveBeenCalledWith(42, undefined);
      expect(result).toBeDefined();
      expect(result?.itemId).toBe(123);
    });

    it('should fetch availability for a single item (Delivery)', async () => {
      const mockResponse = {
        error: null,
        result: [mockAvailabilityDTO],
        message: null,
        invalidProperties: null,
      };

      mockAvailabilityService.AvailabilityShippingAvailability.mockReturnValue(
        of(mockResponse),
      );

      const result = await service.getAvailability({
        orderType: 'Versand',
        item: { itemId: 123, ean: '1234567890', quantity: 1 },
      });

      // Delivery order type filters out logisticianId and supplierId fields
      // to prevent backend from auto-changing orderType to "DIG-Versand"
      const expectedResult = { ...mockAvailabilityDTO };
      delete expectedResult.logisticianId;
      delete expectedResult.supplierId;
      expect(result).toEqual(expectedResult);
    });

    it('should return undefined when item is not available', async () => {
      const mockResponse = {
        error: null,
        result: [], // No availability
        message: null,
        invalidProperties: null,
      };

      mockAvailabilityService.AvailabilityShippingAvailability.mockReturnValue(
        of(mockResponse),
      );

      const result = await service.getAvailability({
        orderType: 'Versand',
        item: { itemId: 123, ean: '1234567890', quantity: 1 },
      });

      expect(result).toBeUndefined();
    });

    it('should throw error for invalid params', async () => {
      await expect(
        service.getAvailability({}),
      ).rejects.toThrow();
    });

    it('should support abort signal', async () => {
      const abortController = new AbortController();
      const mockError = new Error('Request aborted');

      mockAvailabilityService.AvailabilityShippingAvailability.mockReturnValue(
        throwError(() => mockError),
      );

      await expect(
        service.getAvailability(
          {
            orderType: 'Versand',
            item: { itemId: 123, ean: '1234567890', quantity: 1 },
          },
          abortController.signal,
        ),
      ).rejects.toThrow();
    });

    it('should handle Download order type with single item', async () => {
      const mockResponse = {
        error: null,
        result: [mockAvailabilityDTO],
        message: null,
        invalidProperties: null,
      };

      mockAvailabilityService.AvailabilityShippingAvailability.mockReturnValue(
        of(mockResponse),
      );

      const result = await service.getAvailability({
        orderType: 'Download',
        item: { itemId: 123, ean: '1234567890' },
      });

      const callArgs = mockAvailabilityService.AvailabilityShippingAvailability.mock.calls[0][0];
      expect(callArgs[0].qty).toBe(1); // Always 1 for downloads
      expect(result).toEqual(mockAvailabilityDTO);
    });

    it('should handle B2B with single item and logistician override', async () => {
      const mockLogistician = {
        id: 5,
        logisticianNumber: '2470',
        name: 'DHL',
      };

      const mockBranch = {
        id: 42,
        name: 'Test Branch',
        branchNumber: '001',
      };

      mockBranchService.getDefaultBranch.mockResolvedValue(mockBranch);

      mockLogisticianService.LogisticianGetLogisticians.mockReturnValue(
        of({
          error: null,
          result: [mockLogistician],
          message: null,
          invalidProperties: null,
        }),
      );

      const mockResponse = {
        error: null,
        result: [{ ...mockAvailabilityDTO, logisticianId: 99 }],
        message: null,
        invalidProperties: null,
      };

      mockAvailabilityService.AvailabilityStoreAvailability.mockReturnValue(
        of(mockResponse),
      );

      const result = await service.getAvailability({
        orderType: 'B2B-Versand',
        item: { itemId: 123, ean: '1234567890', quantity: 1 },
      });

      expect(mockBranchService.getDefaultBranch).toHaveBeenCalled();
      expect(result?.logisticianId).toBe(5);
    });
  });
});
