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

import { AuthService } from '../../../../core/services/auth/auth.service';
import { CartService } from '../../../../core/services/cart/cart.service';
import { CheckoutService } from '../../../../core/services/checkout.service';
import { CatalogService } from '../../../../core/services/catalog/catalog.service';
import { TenantService } from '../../../../core/services/tenant.service';
import { CheckoutPageComponent } from './checkout-page.component';

describe('CheckoutPageComponent payment validation', () => {
  let checkoutServiceStub: {
    startCheckout: ReturnType<typeof vi.fn>;
    updateCustomerData: ReturnType<typeof vi.fn>;
    updateItemQuantity: ReturnType<typeof vi.fn>;
    prepareItemEditing: ReturnType<typeof vi.fn>;
    cancelPurchase: ReturnType<typeof vi.fn>;
    generatePaymentIntent: ReturnType<typeof vi.fn>;
    getPurchase: ReturnType<typeof vi.fn>;
    submitPurchaseForReview: ReturnType<typeof vi.fn>;
    withCustomLoading: ReturnType<typeof vi.fn>;
  };
  let cartServiceStub: {
    cart: ReturnType<typeof signal>;
    isUpdating: ReturnType<typeof signal<boolean>>;
    loadCart: ReturnType<typeof vi.fn>;
    clearCart: ReturnType<typeof vi.fn>;
  };
  let routerStub: { navigate: ReturnType<typeof vi.fn> };
  let routeQueryParamMap: ReturnType<typeof convertToParamMap>;
  let authUserState: ReturnType<typeof signal>;

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

  beforeEach(async () => {
    vi.useFakeTimers();

    checkoutServiceStub = {
      startCheckout: vi.fn().mockResolvedValue({
        id: 25,
        items: [],
        subtotal: '0.00',
        total: '0.00',
      }),
      updateCustomerData: vi.fn(),
      updateItemQuantity: vi.fn(),
      prepareItemEditing: vi.fn(),
      cancelPurchase: vi.fn().mockResolvedValue({ status: 'cancelled' }),
      generatePaymentIntent: vi.fn().mockResolvedValue({
        qr_data: { qr_code: 'qr-value' },
      }),
      getPurchase: vi.fn().mockResolvedValue({ status: 'pending_payment' }),
      submitPurchaseForReview: vi.fn().mockResolvedValue({ status: 'in_review' }),
      withCustomLoading: vi.fn(),
    };
    checkoutServiceStub.withCustomLoading.mockReturnValue(checkoutServiceStub);
    cartServiceStub = {
      cart: signal({
        id: 10,
        tenant_codigo: 'tenant-test',
        status: 'active',
        items: [],
        subtotal: '0.00',
      }),
      isUpdating: signal(false),
      loadCart: vi.fn().mockReturnValue(of({})),
      clearCart: vi.fn(),
    };
    cartServiceStub.clearCart.mockImplementation(() => {
      cartServiceStub.cart.set({
        id: null,
        tenant_codigo: 'tenant-test',
        status: 'active',
        items: [],
        subtotal: '0.00',
      });
    });
    routerStub = { navigate: vi.fn() };
    routeQueryParamMap = convertToParamMap({});
    authUserState = signal(null);

    await TestBed.configureTestingModule({
      imports: [CheckoutPageComponent],
      providers: [
        { provide: CheckoutService, useValue: checkoutServiceStub },
        { provide: CatalogService, useValue: { getCatalogItem: vi.fn() } },
        { provide: CartService, useValue: cartServiceStub },
        { provide: TenantService, useValue: { tenant: signal({ codigo: 'tenant-test' }) } },
        { provide: AuthService, useValue: { user: authUserState } },
        {
          provide: ActivatedRoute,
          useValue: {
            snapshot: {
              get queryParamMap() {
                return routeQueryParamMap;
              },
            },
          },
        },
        { provide: Router, useValue: routerStub },
      ],
    })
      .overrideComponent(CheckoutPageComponent, { set: { template: '' } })
      .compileComponents();
  });

  afterEach(() => {
    vi.useRealTimers();
    TestBed.resetTestingModule();
  });

  function createComponent(): any {
    const fixture = TestBed.createComponent(CheckoutPageComponent);
    fixture.detectChanges();
    fixture.componentInstance['createdPurchaseId'].set(25);
    routerStub.navigate.mockClear();
    return { fixture, component: fixture.componentInstance as any };
  }

  it('polls QR after five seconds and navigates only when payment is paid', async () => {
    checkoutServiceStub.getPurchase
      .mockResolvedValueOnce({ status: 'pending_payment' })
      .mockResolvedValueOnce({ status: 'paid' });
    const { component } = createComponent();

    await component.selectPaymentMethod('qr');
    expect(checkoutServiceStub.getPurchase).not.toHaveBeenCalled();

    await vi.advanceTimersByTimeAsync(5_000);
    expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(1);
    expect(routerStub.navigate).not.toHaveBeenCalled();

    await vi.advanceTimersByTimeAsync(5_000);
    expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(2);
    expect(cartServiceStub.clearCart).not.toHaveBeenCalled();
    expect(routerStub.navigate).toHaveBeenCalledWith(['/checkout/status', 25]);
    expect(checkoutServiceStub.generatePaymentIntent).toHaveBeenCalledTimes(1);
  });

  it('shows the QR verification state while each status request is in progress', async () => {
    let resolvePurchase!: (purchase: { status: string }) => void;
    checkoutServiceStub.getPurchase.mockReturnValue(
      new Promise((resolve) => {
        resolvePurchase = resolve;
      }),
    );
    const { fixture, component } = createComponent();

    const statusRequest = component.checkQrPayment(component.qrPollingRunId);
    expect(component.isCheckingQrPayment()).toBe(true);

    resolvePurchase({ status: 'pending_payment' });
    await statusRequest;
    expect(component.isCheckingQrPayment()).toBe(false);
    fixture.destroy();
  });

  it('stops QR polling after 45 seconds and can restart it', async () => {
    const { component } = createComponent();

    await component.selectPaymentMethod('qr');
    await vi.advanceTimersByTimeAsync(45_000);

    expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(9);
    expect(component.qrPaymentStatus()).toBe('timed_out');

    component.retryQrPolling();
    expect(component.qrPaymentStatus()).toBe('waiting');
    await vi.advanceTimersByTimeAsync(5_000);
    expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(10);
  });

  it('cancels QR polling when payment method changes or component is destroyed', async () => {
    const first = createComponent();
    await first.component.selectPaymentMethod('qr');
    await first.component.selectPaymentMethod('transfer');
    await vi.advanceTimersByTimeAsync(10_000);
    expect(checkoutServiceStub.getPurchase).not.toHaveBeenCalled();

    const second = createComponent();
    await second.component.selectPaymentMethod('qr');
    second.fixture.destroy();
    await vi.advanceTimersByTimeAsync(10_000);
    expect(checkoutServiceStub.getPurchase).not.toHaveBeenCalled();
  });

  it('checks a transfer once and redirects to purchase status while pending', async () => {
    const { component } = createComponent();
    component.selectedPaymentMethod.set('transfer');

    await component.onComplete();
    expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(1);
    expect(component.transferValidationStatus()).toBe('pending');
    expect(cartServiceStub.clearCart).not.toHaveBeenCalled();
    expect(routerStub.navigate).toHaveBeenCalledWith(['/checkout/status', 25]);

    await vi.advanceTimersByTimeAsync(30_000);
    expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(1);

    await component.onComplete();
    expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(1);
  });

  it('navigates after a transfer is confirmed as paid', async () => {
    checkoutServiceStub.getPurchase.mockResolvedValue({ status: 'paid' });
    const { component } = createComponent();
    component.selectedPaymentMethod.set('transfer');

    await component.onComplete();

    expect(checkoutServiceStub.submitPurchaseForReview).toHaveBeenCalledWith('tenant-test', 25);
    expect(cartServiceStub.clearCart).not.toHaveBeenCalled();
    expect(routerStub.navigate).toHaveBeenCalledWith(['/checkout/status', 25]);
  });

  it('shows a retryable state when transfer validation fails', async () => {
    vi.spyOn(console, 'error').mockImplementation(() => undefined);
    checkoutServiceStub.submitPurchaseForReview.mockRejectedValue(new Error('network error'));
    const { component } = createComponent();
    component.selectedPaymentMethod.set('transfer');

    await component.onComplete();

    expect(component.transferValidationStatus()).toBe('error');
    expect(cartServiceStub.clearCart).not.toHaveBeenCalled();
    expect(routerStub.navigate).not.toHaveBeenCalled();
  });

  it('loads purchase items but prefills customer data from the user', async () => {
    const purchase = {
      id: 25,
      cart_id: null,
      status: 'created',
      payment_method: null,
      transfer_payer_dni: null,
      nombre_apellido: 'Datos de la compra',
      email: 'compra@example.com',
      dni: '11111111',
      telefono: '1111111111',
      items: [
        {
          id: 91,
          quantity: 2,
          unit_price: '1250.50',
          line_total: '2501.00',
          source_catalog_item_id: 8,
          source_variant_id: 21,
          item_details: {
            nombre: 'Remera',
            descripcion: null,
            slug: 'remera',
            imagen: 'https://example.com/remera.jpg',
            attributes: [{ name: 'Color', value: 'Negro' }],
          },
        },
      ],
      subtotal: '2501.00',
      total: '2501.00',
    };
    routeQueryParamMap = convertToParamMap({ purchase: 25 });
    checkoutServiceStub.getPurchase.mockResolvedValue(purchase);
    authUserState.set({
      id: 7,
      nombre_apellido: 'Juan Perez',
      email: 'juan@example.com',
      dni: '12345678',
      telefono: '3415555555',
    });
    const { component } = createComponent();
    await Promise.resolve();

    expect(checkoutServiceStub.getPurchase).toHaveBeenCalledWith('tenant-test', 25);
    expect(component.mappedCartItems()).toEqual([
      {
        cartItemId: 91,
        imageUrl: 'https://example.com/remera.jpg',
        product: 'Remera',
        originalPrice: null,
        discountedPrice: 1250.5,
        discountPercentage: null,
        attributes: [{ label: 'Color', value: 'Negro' }],
        quantity: 2,
      },
    ]);
    expect(component.cartSubtotal()).toBe(2501);
    expect(component.cartTotal()).toBe(2501);
    expect(component.form.getRawValue()).toEqual({
      nombre: 'Juan Perez',
      email: 'juan@example.com',
      dni: '12345678',
      telefono: '3415555555',
    });
  });

  it('keeps the checkout hidden while the purchase is loading', async () => {
    let resolvePurchase!: (purchase: any) => void;
    routeQueryParamMap = convertToParamMap({ purchase: 25 });
    checkoutServiceStub.getPurchase.mockReturnValue(
      new Promise((resolve) => {
        resolvePurchase = resolve;
      }),
    );
    const { component } = createComponent();

    expect(component.isLoadingPurchase()).toBe(true);

    resolvePurchase({
      id: 25,
      status: 'created',
      payment_method: null,
      transfer_payer_dni: null,
      items: [],
      subtotal: '0.00',
      total: '0.00',
    });
    await Promise.resolve();

    expect(component.isLoadingPurchase()).toBe(false);
    expect(component.checkoutStepIndex()).toBe(0);
  });

  it('opens a pending purchase on the payment step and restores its payment method', async () => {
    routeQueryParamMap = convertToParamMap({ purchase: 25 });
    checkoutServiceStub.getPurchase.mockResolvedValue({
      id: 25,
      status: 'pending_payment',
      payment_method: 'transfer',
      transfer_payer_dni: '12345678',
      items: [],
      subtotal: '100.00',
      total: '100.00',
    });
    const { component } = createComponent();
    await Promise.resolve();

    expect(component.checkoutStepIndex()).toBe(1);
    expect(component.selectedPaymentMethod()).toBe('transfer');
    expect(component.transferDni()).toBe('12345678');
    expect(component.isLoadingPurchase()).toBe(false);
  });

  it('generates a new QR when reopening a pending QR purchase', async () => {
    routeQueryParamMap = convertToParamMap({ purchase: 25 });
    checkoutServiceStub.getPurchase.mockResolvedValue({
      id: 25,
      status: 'pending_payment',
      payment_method: 'qr',
      transfer_payer_dni: null,
      items: [],
      subtotal: '100.00',
      total: '100.00',
    });
    const { component } = createComponent();
    await Promise.resolve();
    await Promise.resolve();

    expect(component.checkoutStepIndex()).toBe(1);
    expect(component.selectedPaymentMethod()).toBe('qr');
    expect(checkoutServiceStub.generatePaymentIntent).toHaveBeenCalledWith(
      'tenant-test',
      25,
      'qr',
    );
    expect(component.qrData()).toBe('qr-value');
    expect(component.qrPaymentStatus()).toBe('waiting');
  });

  it.each(['in_review', 'paid', 'cancelled', 'rejected', 'expired'])(
    'redirects a %s purchase to its status page',
    async (status) => {
      routeQueryParamMap = convertToParamMap({ purchase: 25 });
      checkoutServiceStub.getPurchase.mockResolvedValue({
        id: 25,
        status,
        payment_method: null,
        transfer_payer_dni: null,
        items: [],
        subtotal: '100.00',
        total: '100.00',
      });
      createComponent();
      await Promise.resolve();

      expect(routerStub.navigate).toHaveBeenCalledWith(['/checkout/status', 25]);
    },
  );

  it('updates a purchase item while editing and refreshes checkout totals', async () => {
    const updatedPurchase = {
      id: 25,
      items: [],
      subtotal: '300.00',
      total: '300.00',
    };
    checkoutServiceStub.updateItemQuantity.mockResolvedValue(updatedPurchase);
    const { component } = createComponent();
    component.isEditingItems.set(true);

    await component.onPurchaseItemQuantityChange({
      item: {
        cartItemId: 91,
        imageUrl: null,
        product: 'Remera',
        originalPrice: null,
        discountedPrice: 100,
        discountPercentage: null,
        attributes: [],
        quantity: 2,
      },
      quantity: 3,
    });

    expect(checkoutServiceStub.updateItemQuantity).toHaveBeenCalledWith('tenant-test', 25, 91, 3);
    expect(component.createdPurchase()).toBe(updatedPurchase);
    expect(component.isUpdatingItem()).toBe(false);
  });

  it('keeps the payment step selected while editing and regenerates payment afterward', async () => {
    const editablePurchase = {
      id: 25,
      status: 'created',
      items: [],
      subtotal: '100.00',
      total: '100.00',
    };
    checkoutServiceStub.prepareItemEditing.mockResolvedValue(editablePurchase);
    const { component } = createComponent();
    component.stepper = { currentStepIndex: signal(1) };
    const selectPaymentMethod = vi
      .spyOn(component, 'selectPaymentMethod')
      .mockResolvedValue(undefined);

    await component.onEditingItemsChange(true);

    expect(checkoutServiceStub.prepareItemEditing).toHaveBeenCalledWith('tenant-test', 25);
    expect(component.isEditingItems()).toBe(true);
    expect(component.createdPurchase()).toBe(editablePurchase);

    await component.onEditingItemsChange(false);

    expect(component.stepper.currentStepIndex()).toBe(1);
    expect(selectPaymentMethod).toHaveBeenCalledWith('qr');
    expect(component.isEditingItems()).toBe(false);
  });

  it('updates customer data on the existing purchase before payment', async () => {
    const updatedPurchase = {
      id: 25,
      cart_id: 10,
      items: [],
      subtotal: '100.00',
      total: '100.00',
    };
    checkoutServiceStub.updateCustomerData.mockResolvedValue(updatedPurchase);
    const { component } = createComponent();
    component.form.setValue({
      nombre: 'Juan Perez',
      email: 'juan@example.com',
      dni: '12345678',
      telefono: '3415555555',
    });
    component.stepper = { next: vi.fn() };

    await component.onStep1Continue();

    expect(checkoutServiceStub.startCheckout).not.toHaveBeenCalled();
    expect(checkoutServiceStub.updateCustomerData).toHaveBeenCalledWith('tenant-test', 25, {
      dni: '12345678',
      telefono: '3415555555',
      email: 'juan@example.com',
      nombre_apellido: 'Juan Perez',
    });
    expect(component.createdPurchase()).toEqual({
      ...updatedPurchase,
      status: 'pending_payment',
    });
    expect(component.stepper.next).toHaveBeenCalledOnce();
  });

  it('cancels the pending purchase before allowing navigation away', async () => {
    const { component } = createComponent();

    await expect(component.canDeactivate()).resolves.toBe(true);

    expect(checkoutServiceStub.cancelPurchase).toHaveBeenCalledWith('tenant-test', 25);
    expect(cartServiceStub.loadCart).toHaveBeenCalled();
    expect(cartServiceStub.clearCart).not.toHaveBeenCalled();
    expect(component.createdPurchaseId()).toBeNull();
  });
});
