import '@angular/compiler';
import { HttpErrorResponse } from '@angular/common/http';
import { signal } from '@angular/core';
import { TestBed, getTestBed } from '@angular/core/testing';
import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
import { of, Subject, throwError } from 'rxjs';
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';

import { CartService } from '../../../core/services/cart/cart.service';
import { CatalogService } from '../../../core/services/catalog/catalog.service';
import { ModalService } from '../../../core/services/modal.service';
import { ToastService } from '../../../core/services/toast.service';
import { ProductTicketSelectorComponent } from './product-ticket-selector.component';

const attributes = [
  { key: 'tipo', label: 'Tipo' },
  { key: 'sector', label: 'Sector' },
  { key: 'fila', label: 'Fila' },
  { key: 'asiento', label: 'Asiento' },
];

function variant(id: number, tipo: string, sector: string, fila: string, asiento: string) {
  return {
    id,
    precio: '10000.00',
    stock_tecnico: 1,
    values: { tipo, sector, fila, asiento },
  };
}

function mapResponse(variants: ReturnType<typeof variant>[]) {
  return {
    variants,
    selectors: attributes.map(({ key, label }, index) => ({
      key,
      label,
      options: index === 0 ? ['general'] : [],
      enabled: index === 0,
    })),
    selected_values: {},
    resolved_variant: null,
    valid: variants.length > 0,
    available_variant_count: variants.length,
    matching_variant_count: variants.length,
    price_range: { minimum: '10000.00', maximum: '10000.00' },
  };
}

function cartWith(item: ReturnType<typeof variant> | null = null) {
  return {
    id: 10,
    tenant_codigo: 'demo',
    status: 'active',
    subtotal: item === null ? '0.00' : '10000.00',
    items:
      item === null
        ? []
        : [
            {
              id: 25,
              cantidad: 1,
              precio_unitario: '10000.00',
              catalog_item_id: 7,
              variant_id: item.id,
              nombre: 'Entrada',
              imagen: null,
              variant: item,
            },
          ],
  };
}

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

  afterEach(() => TestBed.resetTestingModule());

  async function createComponent({
    maps,
    addItem = vi.fn(),
    cartState = signal(cartWith()),
    toastDanger = vi.fn(),
  }: {
    maps: ReturnType<typeof mapResponse>[];
    addItem?: ReturnType<typeof vi.fn>;
    cartState?: ReturnType<typeof signal<ReturnType<typeof cartWith>>>;
    toastDanger?: ReturnType<typeof vi.fn>;
  }) {
    const getVariantOptions = vi.fn();
    maps.forEach((response) => getVariantOptions.mockReturnValueOnce(of(response)));
    const catalogService = { withoutLoading: vi.fn(), getVariantOptions };
    const cartService = {
      cart: cartState.asReadonly(),
      withoutLoading: vi.fn(),
      addItem,
      updateItemVariant: vi.fn(),
      removeItem: vi.fn().mockReturnValue(of({ data: cartWith() })),
    };
    catalogService.withoutLoading.mockReturnValue(catalogService);
    cartService.withoutLoading.mockReturnValue(cartService);

    await TestBed.configureTestingModule({
      imports: [ProductTicketSelectorComponent],
      providers: [
        { provide: CatalogService, useValue: catalogService },
        { provide: CartService, useValue: cartService },
        {
          provide: ModalService,
          useValue: { openConfirmDelete: vi.fn().mockReturnValue(of(true)) },
        },
        { provide: ToastService, useValue: { danger: toastDanger } },
      ],
    }).compileComponents();

    const fixture = TestBed.createComponent(ProductTicketSelectorComponent);
    fixture.componentRef.setInput('productId', 7);
    fixture.componentRef.setInput('title', 'Entrada');
    fixture.detectChanges();
    await fixture.whenStable();
    fixture.detectChanges();

    return { fixture, getVariantOptions, cartService, toastDanger };
  }

  it('loads one map, recalculates the cascade locally and reserves only after the last select', async () => {
    const selectedVariant = variant(401, 'general', 'A', '1', '1');
    const variants = [
      selectedVariant,
      variant(402, 'general', 'A', '1', '2'),
      variant(403, 'general', 'B', '2', '1'),
    ];
    const addItem = vi.fn().mockReturnValue(of({ data: cartWith(selectedVariant) }));
    const { fixture, getVariantOptions } = await createComponent({
      maps: [mapResponse(variants)],
      addItem,
    });
    const component = fixture.componentInstance;

    expect(getVariantOptions).toHaveBeenCalledTimes(1);
    expect(getVariantOptions).toHaveBeenCalledWith(7, { selected_values: {} });

    component['onSelectionChange'](1, 'tipo', 'general');
    component['onSelectionChange'](1, 'sector', 'A');
    component['onSelectionChange'](1, 'fila', '1');

    expect(getVariantOptions).toHaveBeenCalledTimes(1);
    expect(addItem).not.toHaveBeenCalled();
    expect(component['rows']()[0].selectors[3]).toMatchObject({
      key: 'asiento',
      options: ['1', '2'],
      enabled: true,
    });

    component['onSelectionChange'](1, 'asiento', '1');

    expect(getVariantOptions).toHaveBeenCalledTimes(1);
    expect(addItem).toHaveBeenCalledWith(7, 401, 1);
    expect(component['rows']()[0]).toMatchObject({
      variantId: 401,
      reservedVariantId: 401,
      status: 'reserved',
    });
  });

  it('shows a saving status while the selected seat is being reserved', async () => {
    const selectedVariant = variant(401, 'general', 'A', '1', '1');
    const reservation = new Subject<{ data: ReturnType<typeof cartWith> }>();
    const addItem = vi.fn().mockReturnValue(reservation);
    const { fixture } = await createComponent({
      maps: [mapResponse([selectedVariant])],
      addItem,
    });
    const component = fixture.componentInstance;

    component['onSelectionChange'](1, 'tipo', 'general');
    component['onSelectionChange'](1, 'sector', 'A');
    component['onSelectionChange'](1, 'fila', '1');
    component['onSelectionChange'](1, 'asiento', '1');
    fixture.detectChanges();

    const savingStatus = fixture.nativeElement.querySelector(
      '.ticket-selector__row-status',
    ) as HTMLElement | null;
    expect(savingStatus?.textContent?.trim()).toBe('Guardando...');
    expect(savingStatus?.getAttribute('role')).toBe('status');

    reservation.next({ data: cartWith(selectedVariant) });
    reservation.complete();
    fixture.detectChanges();

    expect(fixture.nativeElement.querySelector('.ticket-selector__row-status')).toBeNull();
  });

  it('shows the remove action as a bordered icon button', async () => {
    const { fixture } = await createComponent({
      maps: [mapResponse([variant(401, 'general', 'A', '1', '1')])],
    });

    const removeButton = fixture.nativeElement.querySelector(
      'app-icon-button[ariaLabel="Eliminar entrada"] button',
    ) as HTMLButtonElement | null;

    expect(removeButton?.classList.contains('icon-btn--bordered')).toBe(true);
  });

  it('shows one header row with the label of every selector', async () => {
    const { fixture } = await createComponent({
      maps: [mapResponse([variant(401, 'general', 'A', '1', '1')])],
    });

    const headerRows = fixture.nativeElement.querySelectorAll('.ticket-selector__columns');
    const labels = fixture.nativeElement.querySelectorAll('.ticket-selector__column-label');

    expect(headerRows).toHaveLength(1);
    expect([...labels].map((label: Element) => label.textContent?.trim())).toEqual([
      'Tipo',
      'Sector',
      'Fila',
      'Asiento',
    ]);
  });

  it('clears only the seat after a stock conflict when the row still has alternatives', async () => {
    const failed = variant(401, 'general', 'A', '1', '1');
    const alternative = variant(402, 'general', 'A', '1', '2');
    const message = 'El asiento ya no está disponible.';
    const addItem = vi.fn().mockReturnValue(
      throwError(
        () =>
          new HttpErrorResponse({
            status: 422,
            error: { message },
          }),
      ),
    );
    const { fixture, getVariantOptions, toastDanger } = await createComponent({
      maps: [mapResponse([failed, alternative]), mapResponse([alternative])],
      addItem,
    });
    const component = fixture.componentInstance;

    component['onSelectionChange'](1, 'tipo', 'general');
    component['onSelectionChange'](1, 'sector', 'A');
    component['onSelectionChange'](1, 'fila', '1');
    component['onSelectionChange'](1, 'asiento', '1');

    expect(getVariantOptions).toHaveBeenCalledTimes(2);
    expect(component['rows']()[0]).toMatchObject({
      selectedValues: { tipo: 'general', sector: 'A', fila: '1' },
      variantId: null,
      status: 'selecting',
      error: message,
    });
    expect(component['rows']()[0].selectors[3].options).toEqual(['2']);
    expect(toastDanger).toHaveBeenCalledWith(message);
  });

  it('also clears the row when the refreshed map has no seats for that row', async () => {
    const failed = variant(401, 'general', 'A', '1', '1');
    const otherRow = variant(402, 'general', 'A', '2', '1');
    const addItem = vi
      .fn()
      .mockReturnValue(
        throwError(() => new HttpErrorResponse({ status: 422, error: { message: 'Sin stock.' } })),
      );
    const { fixture } = await createComponent({
      maps: [mapResponse([failed, otherRow]), mapResponse([otherRow])],
      addItem,
    });
    const component = fixture.componentInstance;

    component['onSelectionChange'](1, 'tipo', 'general');
    component['onSelectionChange'](1, 'sector', 'A');
    component['onSelectionChange'](1, 'fila', '1');
    component['onSelectionChange'](1, 'asiento', '1');

    expect(component['rows']()[0].selectedValues).toEqual({ tipo: 'general', sector: 'A' });
    expect(component['rows']()[0].selectors[2]).toMatchObject({
      key: 'fila',
      options: ['2'],
      enabled: true,
    });
    expect(component['rows']()[0].selectors[3].enabled).toBe(false);
  });

  it('restores a variant already reserved in the cart without another options request', async () => {
    const reserved = variant(401, 'general', 'A', '1', '1');
    const cartState = signal(cartWith(reserved));
    const { fixture, getVariantOptions, cartService } = await createComponent({
      maps: [mapResponse([])],
      cartState,
    });

    expect(getVariantOptions).toHaveBeenCalledTimes(1);
    expect(cartService.updateItemVariant).not.toHaveBeenCalled();
    expect(fixture.componentInstance['rows']()[0]).toMatchObject({
      variantId: 401,
      reservedVariantId: 401,
      cartItemId: 25,
      selectedValues: { tipo: 'general', sector: 'A', fila: '1', asiento: '1' },
      status: 'reserved',
    });
  });
});
