import { TestBed, getTestBed } from '@angular/core/testing';
import { signal } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
import { ActivatedRoute, Router } from '@angular/router';
import { By } from '@angular/platform-browser';
import { BehaviorSubject, of, throwError } from 'rxjs';
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';

import { CatalogItemDetail } from '../../../../core/services/catalog/catalog.interface';
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
import { CatalogAvailabilityService } from '../../../../core/services/catalog/catalog-availability.service';
import { CartService } from '../../../../core/services/cart/cart.service';
import { ToastService } from '../../../../core/services/toast.service';
import { ProductDetailPageComponent } from './product-detail-page.component';
import { CheckoutService } from '../../../../core/services/checkout.service';
import { TenantService } from '../../../../core/services/tenant.service';
import { AuthService } from '../../../../core/services/auth/auth.service';
import {
  PRODUCT_DETAIL_ERROR_MESSAGE,
  ProductDetailResolvedData,
} from './product-detail-page.resolver';

describe('ProductDetailPageComponent', () => {
  const mockProduct: CatalogItemDetail = {
    id: 1,
    type: 'product',
    category_id: 10,
    brand_id: null,
    slug: 'auriculares-bluetooth',
    nombre: 'Auriculares Bluetooth',
    descripcion: 'Auriculares bluetooth de prueba',
    precio: '24999',
    category: 'Tecnología',
    brand: 'Sony',
    images: ['https://example.com/image.png'],
    inventory_policy: 'tracked',
    has_tickets: false,
    minimum_use_date: null,
    maximum_use_date: null,
    maximum_addable_quantity: 10,
    attributes: [],
    variants: [],
  };

  let routeDataSubject: BehaviorSubject<{ productDetailData: ProductDetailResolvedData }>;
  let catalogServiceStub: any;
  let routerStub: any;
  let cartServiceStub: any;
  let toastServiceStub: any;
  let checkoutServiceStub: any;

  beforeAll(() => {
    try {
      getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting());
    } catch {
      // Test environment may already be initialized by another setup entrypoint.
    }
  });

  beforeEach(() => {
    vi.restoreAllMocks();
    routeDataSubject = new BehaviorSubject<{ productDetailData: ProductDetailResolvedData }>({
      productDetailData: {
        product: mockProduct,
        error: null,
      },
    });
    catalogServiceStub = {
      getCatalogItem: vi.fn(),
      withCustomLoading: vi.fn(),
    };
    catalogServiceStub.withCustomLoading.mockReturnValue(catalogServiceStub);
    routerStub = {
      navigate: vi.fn(),
    };
    cartServiceStub = {
      addItem: vi.fn().mockReturnValue(of({ message: 'Producto agregado al carrito' })),
      withCustomLoading: vi.fn(),
    };
    cartServiceStub.withCustomLoading.mockReturnValue(cartServiceStub);
    toastServiceStub = {
      success: vi.fn(),
      danger: vi.fn(),
      info: vi.fn(),
    };
    checkoutServiceStub = {
      startCheckout: vi.fn().mockResolvedValue({ id: 44 }),
    };
  });

  async function configureTestingModule() {
    await TestBed.configureTestingModule({
      imports: [ProductDetailPageComponent],
      providers: [
        {
          provide: ActivatedRoute,
          useValue: {
            data: routeDataSubject.asObservable(),
          },
        },
        {
          provide: CatalogService,
          useValue: catalogServiceStub,
        },
        {
          provide: Router,
          useValue: routerStub,
        },
        {
          provide: CartService,
          useValue: cartServiceStub,
        },
        {
          provide: ToastService,
          useValue: toastServiceStub,
        },
        {
          provide: CheckoutService,
          useValue: checkoutServiceStub,
        },
        {
          provide: TenantService,
          useValue: { tenant: signal({ codigo: 'tenant-test' }) },
        },
        {
          provide: AuthService,
          useValue: {
            user: signal({
              id: 7,
              nombre_apellido: 'Juan Perez',
              email: 'juan@example.com',
              dni: '12345678',
              telefono: '3415555555',
            }),
          },
        },
      ],
    }).compileComponents();
  }

  function resolveProduct(product: CatalogItemDetail): void {
    routeDataSubject.next({
      productDetailData: {
        product,
        error: null,
      },
    });
  }

  function resolveProductError(error: string): void {
    routeDataSubject.next({
      productDetailData: {
        product: null,
        error,
      },
    });
  }

  it('renders the resolved product and embeds the carousel', async () => {
    await configureTestingModule();
    const fixture = TestBed.createComponent(ProductDetailPageComponent);
    fixture.detectChanges();

    const element = fixture.nativeElement as HTMLElement;

    expect(catalogServiceStub.getCatalogItem).not.toHaveBeenCalled();

    expect(element.querySelector('app-product-carousel')).not.toBeNull();
    expect(element.querySelector('.product-detail__title')?.textContent).toContain(
      'Auriculares Bluetooth',
    );
    expect(element.querySelector('.product-detail__price-current')?.textContent).toContain(
      '$24.999',
    );
    expect(element.querySelector('.product-detail__price-previous')).toBeNull();
    expect(element.querySelector('.product-carousel__discount-badge')).toBeNull();
  });

  it('reloads product availability when the cart changes', async () => {
    catalogServiceStub.getCatalogItem.mockReturnValue(
      of({ ...mockProduct, maximum_addable_quantity: 4 }),
    );
    await configureTestingModule();
    const fixture = TestBed.createComponent(ProductDetailPageComponent);
    fixture.detectChanges();

    TestBed.inject(CatalogAvailabilityService).notifyAvailabilityChanged();
    fixture.detectChanges();

    expect(catalogServiceStub.getCatalogItem).toHaveBeenCalledWith(1, undefined);
    expect(fixture.componentInstance['selectedVariantMax']()).toBe(4);
  });

  it('shows error message if the resolver cannot load the product', async () => {
    resolveProductError(PRODUCT_DETAIL_ERROR_MESSAGE);
    await configureTestingModule();
    const fixture = TestBed.createComponent(ProductDetailPageComponent);
    fixture.detectChanges();

    const element = fixture.nativeElement as HTMLElement;

    expect(element.textContent).toContain('No pudimos cargar los detalles del producto.');
  });

  it('should use default variant images if present', async () => {
    const detailProduct: CatalogItemDetail = {
      ...mockProduct,
      images: ['https://example.com/product.png'],
      variants: [{ id: 123, maximum_addable_quantity: 10, values: {} }],
      selected_variant: {
        id: 123,
        maximum_addable_quantity: 10,
        images: ['https://example.com/variant1.png', 'https://example.com/variant2.png'],
        values: {},
      },
    };
    resolveProduct(detailProduct);

    await configureTestingModule();
    const fixture = TestBed.createComponent(ProductDetailPageComponent);
    fixture.detectChanges();

    const carousel = fixture.debugElement.query(By.css('app-product-carousel')).componentInstance;
    expect(carousel.images()).toEqual([
      'https://example.com/variant1.png',
      'https://example.com/variant2.png',
    ]);
  });

  it('should fallback to product images if default variant images are not present', async () => {
    const detailProduct: CatalogItemDetail = {
      ...mockProduct,
      images: ['https://example.com/product.png'],
    };
    resolveProduct(detailProduct);

    await configureTestingModule();
    const fixture = TestBed.createComponent(ProductDetailPageComponent);
    fixture.detectChanges();

    const carousel = fixture.debugElement.query(By.css('app-product-carousel')).componentInstance;
    expect(carousel.images()).toEqual(['https://example.com/product.png']);
  });

  it('should fallback to empty array if no images are present at all', async () => {
    const detailProduct: CatalogItemDetail = {
      ...mockProduct,
      images: [],
    };
    resolveProduct(detailProduct);

    await configureTestingModule();
    const fixture = TestBed.createComponent(ProductDetailPageComponent);
    fixture.detectChanges();

    const carousel = fixture.debugElement.query(By.css('app-product-carousel')).componentInstance;
    expect(carousel.images()).toEqual([]);
  });

  it('renders generic attributes and preselects default variant options', async () => {
    const detailProduct: CatalogItemDetail = {
      ...mockProduct,
      attributes: [
        {
          id: 1,
          codigo: 'color',
          nombre: 'Color',
          is_required: true,
          metadata_schema: null,
          type: 'select',
          options: [
            {
              id: 10,
              value: 'beige',
              label: 'Beige',
              sort_order: 1,
              metadata: { hex: '#D8D1C7' },
            },
            {
              id: 11,
              value: 'brown',
              label: 'Marrón',
              sort_order: 2,
              metadata: { palette: { primary: '#7E6460' } },
            },
          ],
        },
        {
          id: 2,
          codigo: 'material',
          nombre: 'Material',
          is_required: true,
          metadata_schema: null,
          type: 'select',
          options: [
            {
              id: 20,
              value: 'mesh',
              label: 'Mesh',
              sort_order: 1,
              metadata: null,
            },
            {
              id: 21,
              value: 'cuero',
              label: 'Cuero',
              sort_order: 2,
              metadata: null,
            },
          ],
        },
      ],
      variants: [
        { id: 123, maximum_addable_quantity: 10, values: { color: 'beige', material: 'Cuero' } },
      ],
      selected_variant: {
        id: 123,
        maximum_addable_quantity: 10,
        images: ['https://example.com/variant1.png'],
        values: {
          color: 'beige',
          material: 'Cuero',
        },
      },
    };
    resolveProduct(detailProduct);

    await configureTestingModule();
    const fixture = TestBed.createComponent(ProductDetailPageComponent);
    fixture.detectChanges();

    const element = fixture.nativeElement as HTMLElement;
    const swatches = element.querySelectorAll('.attribute-selector__swatch');
    const textOptions = element.querySelectorAll('.attribute-selector__text-option');
    const labels = Array.from(element.querySelectorAll('.attribute-selector__label')).map((label) =>
      label.textContent?.trim(),
    );

    expect(labels).toEqual(['Color:', 'Material:']);
    expect(swatches).toHaveLength(2);
    expect(textOptions).toHaveLength(2);
    expect(swatches[0].classList.contains('attribute-selector__swatch--selected')).toBe(true);
    expect(textOptions[1].classList.contains('attribute-selector__text-option--selected')).toBe(
      true,
    );
    expect((swatches[0] as HTMLElement).style.backgroundColor).not.toBe('');
  });

  it('renders event dates as a dynamic attribute and resolves their variants', async () => {
    const detailProduct: CatalogItemDetail = {
      ...mockProduct,
      purpose: 'entry',
      has_tickets: true,
      variants: [
        { id: 101, event_date_id: 20, maximum_addable_quantity: 10, values: { event_date: '20' } },
        { id: 102, event_date_id: 21, maximum_addable_quantity: 10, values: { event_date: '21' } },
      ],
      attributes: [
        {
          id: 99,
          codigo: 'event_date',
          nombre: 'Fecha',
          is_required: true,
          metadata_schema: null,
          type: 'event_date',
          options: [
            {
              id: 20,
              value: '20',
              label: '09/10/2026',
              sort_order: 0,
              metadata: null,
            },
            {
              id: 21,
              value: '21',
              label: '10/10/2026',
              sort_order: 1,
              metadata: null,
            },
          ],
        },
      ],
      selected_variant: {
        id: 101,
        event_date_id: 20,
        maximum_addable_quantity: 10,
        images: [],
        values: {},
      },
    };
    resolveProduct(detailProduct);
    catalogServiceStub.getCatalogItem.mockReturnValue(
      of({ ...detailProduct, selected_variant: { ...detailProduct.selected_variant!, id: 102 } }),
    );

    await configureTestingModule();
    const fixture = TestBed.createComponent(ProductDetailPageComponent);
    fixture.detectChanges();

    const options = fixture.nativeElement.querySelectorAll(
      '.attribute-selector__text-option',
    ) as NodeListOf<HTMLButtonElement>;
    expect(options).toHaveLength(2);
    expect(options[0].classList.contains('attribute-selector__text-option--selected')).toBe(true);

    options[1].click();
    fixture.detectChanges();

    expect(catalogServiceStub.getCatalogItem).toHaveBeenCalledWith(1, 102);
  });

  it('hides the attributes block and its extra divider when no attributes are present', async () => {
    await configureTestingModule();
    const fixture = TestBed.createComponent(ProductDetailPageComponent);
    fixture.detectChanges();

    const element = fixture.nativeElement as HTMLElement;

    expect(element.querySelector('.product-detail__section--attributes')).toBeNull();
    expect(element.querySelectorAll('.product-detail__divider')).toHaveLength(2);
  });

  it('keeps quantity at a minimum of one and increments locally', async () => {
    await configureTestingModule();
    const fixture = TestBed.createComponent(ProductDetailPageComponent);
    fixture.detectChanges();

    fixture.componentInstance['selectedVariant'].set({
      id: 1,
      maximum_addable_quantity: 10,
      values: {},
    });
    fixture.detectChanges();

    const element = fixture.nativeElement as HTMLElement;
    const decreaseButton = element.querySelector(
      '[aria-label="Disminuir cantidad"]',
    ) as HTMLButtonElement;
    const increaseButton = element.querySelector(
      '[aria-label="Aumentar cantidad"]',
    ) as HTMLButtonElement;

    decreaseButton.click();
    fixture.detectChanges();

    expect(element.querySelector('.quantity-selector__value')?.textContent?.trim()).toBe('1');

    increaseButton.click();
    increaseButton.click();
    fixture.detectChanges();

    expect(element.querySelector('.quantity-selector__value')?.textContent?.trim()).toBe('3');
  });

  it('toggles the full description state when overflow is available', async () => {
    await configureTestingModule();
    const fixture = TestBed.createComponent(ProductDetailPageComponent);
    fixture.detectChanges();

    (fixture.componentInstance as any).descriptionHasOverflow.set(true);
    fixture.detectChanges();

    const element = fixture.nativeElement as HTMLElement;
    const toggleButton = element.querySelector(
      '.product-detail__description-toggle',
    ) as HTMLButtonElement;

    expect(toggleButton.textContent?.trim()).toBe('Mostrar más');

    toggleButton.click();
    fixture.detectChanges();

    expect(
      element
        .querySelector('.product-detail__description-body')
        ?.classList.contains('product-detail__description-body--expanded'),
    ).toBe(true);
    expect(toggleButton.textContent?.trim()).toBe('Mostrar menos');
  });

  it('renders purchase actions and starts a direct checkout', async () => {
    await configureTestingModule();
    const fixture = TestBed.createComponent(ProductDetailPageComponent);
    fixture.detectChanges();

    const buttons = Array.from(
      fixture.nativeElement.querySelectorAll('app-button button'),
    ) as HTMLButtonElement[];

    expect(buttons.map((button) => button.textContent?.trim())).toEqual([
      'Agregar al carrito',
      'Comprar',
    ]);

    buttons[0].click();
    buttons[1].click();
    await Promise.resolve();

    expect(checkoutServiceStub.startCheckout).toHaveBeenCalledWith('tenant-test', {
      direct_items: [
        {
          catalog_item_id: 1,
          variant_id: null,
          cantidad: 1,
        },
      ],
    });
    expect(routerStub.navigate).toHaveBeenCalledWith(['/checkout', 44]);
  });

  it('shows the backend purchase-limit message for a direct checkout', async () => {
    checkoutServiceStub.startCheckout.mockRejectedValue(
      new HttpErrorResponse({
        status: 422,
        error: {
          code: 'purchase.limit_exceeded',
          message: 'Podés agregar hasta 2 unidades más de “Auriculares Bluetooth”.',
          maximum_addable_quantity: 2,
        },
      }),
    );
    await configureTestingModule();
    const fixture = TestBed.createComponent(ProductDetailPageComponent);
    fixture.detectChanges();
    const buyButton = Array.from(
      fixture.nativeElement.querySelectorAll('app-button button') as NodeListOf<HTMLButtonElement>,
    ).find((button) => button.textContent?.trim() === 'Comprar') as HTMLButtonElement;

    buyButton.click();
    await Promise.resolve();

    expect(toastServiceStub.danger).toHaveBeenCalledWith(
      'Podés agregar hasta 2 unidades más de “Auriculares Bluetooth”.',
    );
  });

  it('calls CartService.addItem when variant is selected and Add to Cart is clicked', async () => {
    const detailProduct: CatalogItemDetail = {
      ...mockProduct,
      variants: [
        {
          id: 123,
          maximum_addable_quantity: 5,
          values: {},
        },
      ],
      selected_variant: {
        id: 123,
        maximum_addable_quantity: 5,
        images: [],
        values: {},
      },
    };
    resolveProduct(detailProduct);

    await configureTestingModule();
    const fixture = TestBed.createComponent(ProductDetailPageComponent);
    fixture.detectChanges();

    // Select the variant
    fixture.componentInstance['selectedVariant'].set(detailProduct.variants[0]);
    fixture.componentInstance['quantity'].set(3);
    fixture.detectChanges();

    const element = fixture.nativeElement as HTMLElement;
    const addToCartButton = Array.from(element.querySelectorAll('app-button button')).find(
      (btn) => btn.textContent?.trim() === 'Agregar al carrito',
    ) as HTMLButtonElement;

    expect(addToCartButton).toBeDefined();
    addToCartButton.click();
    fixture.detectChanges();

    expect(cartServiceStub.addItem).toHaveBeenCalledWith(1, 123, 3);
    expect(toastServiceStub.success).toHaveBeenCalledWith('Producto agregado al carrito');
  });

  it('adds a catalog item without variants to the cart', async () => {
    resolveProduct({
      ...mockProduct,
      variants: [],
      maximum_addable_quantity: 4,
    });

    await configureTestingModule();
    const fixture = TestBed.createComponent(ProductDetailPageComponent);
    fixture.detectChanges();

    const buttons = Array.from(
      fixture.nativeElement.querySelectorAll('app-button button'),
    ) as HTMLButtonElement[];
    const addToCartButton = buttons.find(
      (button) => button.textContent?.trim() === 'Agregar al carrito',
    ) as HTMLButtonElement;

    addToCartButton.click();
    fixture.detectChanges();

    expect(cartServiceStub.addItem).toHaveBeenCalledWith(1, null, 1);
  });

  it('shows error toast when CartService.addItem fails', async () => {
    const detailProduct: CatalogItemDetail = {
      ...mockProduct,
      variants: [
        {
          id: 123,
          maximum_addable_quantity: 5,
          values: {},
        },
      ],
      selected_variant: {
        id: 123,
        maximum_addable_quantity: 5,
        images: [],
        values: {},
      },
    };
    resolveProduct(detailProduct);
    cartServiceStub.addItem.mockReturnValue(throwError(() => new Error('Failed to add')));

    await configureTestingModule();
    const fixture = TestBed.createComponent(ProductDetailPageComponent);
    fixture.detectChanges();

    fixture.componentInstance['selectedVariant'].set(detailProduct.variants[0]);
    fixture.detectChanges();

    const element = fixture.nativeElement as HTMLElement;
    const addToCartButton = Array.from(element.querySelectorAll('app-button button')).find(
      (btn) => btn.textContent?.trim() === 'Agregar al carrito',
    ) as HTMLButtonElement;

    addToCartButton.click();
    fixture.detectChanges();

    expect(cartServiceStub.addItem).toHaveBeenCalled();
    expect(toastServiceStub.danger).toHaveBeenCalledWith(
      'No se pudo agregar el producto al carrito.',
    );
  });

  it('allows unlimited variants to increase quantity without a maximum', async () => {
    const unlimitedVariant = {
      id: 321,
      maximum_addable_quantity: null,
      values: {},
    };
    resolveProduct({
      ...mockProduct,
      inventory_policy: 'unlimited',
      selected_variant: {
        id: 321,
        maximum_addable_quantity: null,
        images: [],
        values: {},
      },
      variants: [unlimitedVariant],
    });

    await configureTestingModule();
    const fixture = TestBed.createComponent(ProductDetailPageComponent);
    fixture.detectChanges();

    const increaseButton = fixture.nativeElement.querySelector(
      'app-quantity-selector button:last-child',
    ) as HTMLButtonElement;
    expect(increaseButton.disabled).toBe(false);
    increaseButton.click();
    fixture.detectChanges();

    expect(fixture.componentInstance['quantity']()).toBe(2);
  });

  it('caps an unlimited variant at the per-user purchase limit', async () => {
    const unlimitedVariant = {
      id: 322,
      maximum_addable_quantity: 2,
      values: {},
    };
    resolveProduct({
      ...mockProduct,
      inventory_policy: 'unlimited',
      max_units_per_user: 2,
      selected_variant: {
        id: 322,
        maximum_addable_quantity: 2,
        images: [],
        values: { event_date: '20' },
      },
      variants: [unlimitedVariant],
    });

    await configureTestingModule();
    const fixture = TestBed.createComponent(ProductDetailPageComponent);
    fixture.detectChanges();

    const increaseButton = fixture.nativeElement.querySelector(
      'app-quantity-selector button:last-child',
    ) as HTMLButtonElement;
    increaseButton.click();
    fixture.detectChanges();

    expect(fixture.componentInstance['quantity']()).toBe(2);
    expect(increaseButton.disabled).toBe(true);
  });

  it('disables purchase actions for tracked variants without stock', async () => {
    const trackedVariant = {
      id: 654,
      maximum_addable_quantity: 0,
      values: {},
    };
    resolveProduct({
      ...mockProduct,
      selected_variant: {
        id: 654,
        maximum_addable_quantity: 0,
        images: [],
        values: {},
      },
      variants: [trackedVariant],
    });

    await configureTestingModule();
    const fixture = TestBed.createComponent(ProductDetailPageComponent);
    fixture.detectChanges();

    const purchaseButtons = Array.from(
      fixture.nativeElement.querySelectorAll('app-button button'),
    ) as HTMLButtonElement[];
    expect(purchaseButtons).toHaveLength(2);
    expect(purchaseButtons.every((button) => button.disabled)).toBe(true);
  });
});
