import { TestBed } from '@angular/core/testing';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { provideHttpClient } from '@angular/common/http';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';

import { CartService } from './cart.service';
import { TenantService } from '../tenant.service';
import { Cart } from './cart.interface';
import { LOADING_MODE } from '../global-loading/loading-mode';
import { CatalogAvailabilityService } from '../catalog/catalog-availability.service';

describe('CartService', () => {
  let service: CartService;
  let httpMock: HttpTestingController;
  let tenantServiceMock: any;
  let catalogAvailabilityService: CatalogAvailabilityService;

  const mockCart: Cart = {
    id: 123,
    tenant_codigo: 'acme',
    status: 'active',
    items: [
      {
        id: 1,
        cantidad: 2,
        precio_unitario: '10.00',
        catalog_item_id: 5,
        variant_id: 10,
        nombre: 'Test Product (Size: M)',
        imagen: null,
        variant: {
          id: 10,
          precio: '10.00',
          stock_tecnico: 1,
          values: { size: 'M' },
        },
      },
    ],
    subtotal: '20.00',
  };

  beforeEach(() => {
    tenantServiceMock = {
      getTenantApiUrl: vi.fn().mockReturnValue('http://api.test/tenants/acme'),
      tenant: vi.fn().mockReturnValue({ codigo: 'acme' }),
    };

    TestBed.configureTestingModule({
      providers: [
        CartService,
        provideHttpClient(),
        provideHttpClientTesting(),
        { provide: TenantService, useValue: tenantServiceMock },
      ],
    });

    service = TestBed.inject(CartService);
    catalogAvailabilityService = TestBed.inject(CatalogAvailabilityService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();
  });

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

  it('should initialize with null cart signal', () => {
    expect(service.cart()).toBeNull();
  });

  it('should load cart and update signal', () => {
    service.loadCart().subscribe((cart) => {
      expect(cart).toEqual(mockCart);
      expect(service.cart()).toEqual(mockCart);
    });

    const req = httpMock.expectOne('http://api.test/tenants/acme/cart');
    expect(req.request.method).toBe('GET');
    expect(req.request.withCredentials).toBe(true);
    req.flush({ data: mockCart });
  });

  it('refreshes catalog availability only after the expired cart has been reloaded', () => {
    const availabilityChanged = vi.fn();
    catalogAvailabilityService.availabilityChanged$.subscribe(availabilityChanged);

    service.loadCart(true).subscribe();

    expect(availabilityChanged).not.toHaveBeenCalled();
    const req = httpMock.expectOne('http://api.test/tenants/acme/cart');
    req.flush({ data: mockCart });

    expect(service.cart()).toEqual(mockCart);
    expect(availabilityChanged).toHaveBeenCalledOnce();
  });

  it('propagates a custom loading mode to the request context', () => {
    service.withCustomLoading().loadCart().subscribe();

    const req = httpMock.expectOne('http://api.test/tenants/acme/cart');
    expect(req.request.context.get(LOADING_MODE)).toBe('custom');
    req.flush({ data: mockCart });
  });

  it('should clear cart state locally', () => {
    service.clearCart();

    expect(service.cart()).toEqual({
      id: null,
      tenant_codigo: 'acme',
      status: 'active',
      items: [],
      subtotal: '0.00',
    });
  });

  it('should add item and update signal', () => {
    const availabilityChanged = vi.fn();
    catalogAvailabilityService.availabilityChanged$.subscribe(availabilityChanged);

    service.addItem(5, 10, 2).subscribe((res) => {
      expect(res.data).toEqual(mockCart);
      expect(service.cart()).toEqual(mockCart);
    });

    const req = httpMock.expectOne('http://api.test/tenants/acme/cart/items');
    expect(req.request.method).toBe('POST');
    expect(req.request.body).toEqual({
      catalog_item_id: 5,
      variant_id: 10,
      cantidad: 2,
    });
    expect(req.request.withCredentials).toBe(true);
    req.flush({ data: mockCart });
    expect(availabilityChanged).toHaveBeenCalledOnce();
  });

  it('should update item quantity and update signal', () => {
    const availabilityChanged = vi.fn();
    catalogAvailabilityService.availabilityChanged$.subscribe(availabilityChanged);

    const updatedCart = { ...mockCart, subtotal: '30.00' };
    updatedCart.items[0].cantidad = 3;

    service.updateItemQuantity(10, 3).subscribe((res) => {
      expect(res.data).toEqual(updatedCart);
      expect(service.cart()).toEqual(updatedCart);
    });

    const req = httpMock.expectOne('http://api.test/tenants/acme/cart/items/10');
    expect(req.request.method).toBe('PATCH');
    expect(req.request.body).toEqual({ cantidad: 3 });
    expect(req.request.withCredentials).toBe(true);
    req.flush({ data: updatedCart });
    expect(availabilityChanged).toHaveBeenCalledOnce();
  });

  it('should remove item and update signal', () => {
    const availabilityChanged = vi.fn();
    catalogAvailabilityService.availabilityChanged$.subscribe(availabilityChanged);

    const emptyCart: Cart = {
      id: 123,
      tenant_codigo: 'acme',
      status: 'active',
      items: [],
      subtotal: '0.00',
    };

    service.removeItem(10).subscribe((res) => {
      expect(res.data).toEqual(emptyCart);
      expect(service.cart()).toEqual(emptyCart);
    });

    const req = httpMock.expectOne('http://api.test/tenants/acme/cart/items/10');
    expect(req.request.method).toBe('DELETE');
    expect(req.request.withCredentials).toBe(true);
    req.flush({ data: emptyCart });
    expect(availabilityChanged).toHaveBeenCalledOnce();
  });

  it('does not notify an availability change when a cart mutation fails', () => {
    const availabilityChanged = vi.fn();
    catalogAvailabilityService.availabilityChanged$.subscribe(availabilityChanged);

    service.removeItem(10).subscribe({ error: vi.fn() });

    const req = httpMock.expectOne('http://api.test/tenants/acme/cart/items/10');
    req.flush({ message: 'Error' }, { status: 500, statusText: 'Server Error' });

    expect(availabilityChanged).not.toHaveBeenCalled();
  });
});
