import { signal } from '@angular/core';
import { By } from '@angular/platform-browser';
import { TestBed } from '@angular/core/testing';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { TenantService } from '../../../../../../core/services/tenant.service';
import { ToastService } from '../../../../../../core/services/toast.service';
import { ModalService } from '../../../../../../core/services/modal.service';
import { TicketComponent } from './components/ticket/ticket.component';
import { TicketResponse, TicketService } from './ticket.service';
import { TicketsPage } from './tickets-page';

function withCustomLoading<T extends object>(service: T): T & { withCustomLoading: () => T } {
  return Object.assign(service, {
    withCustomLoading: () => service,
  });
}

const ticket = (overrides: Partial<TicketResponse>): TicketResponse => ({
  id: 1,
  tenant_code: 'tenant',
  ticket: 'ticket-1',
  name: 'Entrada general',
  description: null,
  source_catalog_item_id: null,
  source_variant_id: null,
  starts_at: '2026-10-09T00:00:00.000Z',
  expires_at: null,
  used_at: null,
  is_valid: true,
  is_expired: false,
  is_used: false,
  ...overrides,
});

describe('TicketsPage', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        {
          provide: TenantService,
          useValue: {
            tenant: signal({
              menues: [
                {
                  id: 4,
                  code: 'account.tickets',
                  label: 'Entradas disponibles',
                  parent_menu_code: 'account',
                  content_type: 'dynamic',
                  route: '/mi-cuenta/tickets',
                  submenues: [],
                },
              ],
            }).asReadonly(),
          },
        },
      ],
    });
  });

  it('shares the generated PDF as a file without text that Windows could prioritize', async () => {
    const share = vi.fn().mockResolvedValue(undefined);
    const canShare = vi.fn().mockReturnValue(true);
    vi.stubGlobal('navigator', { canShare, share });

    await TestBed.configureTestingModule({
      imports: [TicketsPage],
      providers: [{ provide: ToastService, useValue: { danger: vi.fn() } }],
    })
      .overrideComponent(TicketsPage, {
        set: {
          providers: [
            {
              provide: TicketService,
              useValue: withCustomLoading({
                getTickets: () => Promise.resolve([ticket({})]),
                downloadPdf: () => Promise.resolve(new Blob(['pdf-content'])),
              }),
            },
          ],
        },
      })
      .compileComponents();

    const fixture = TestBed.createComponent(TicketsPage);
    fixture.detectChanges();
    await fixture.whenStable();
    fixture.detectChanges();

    const page = fixture.nativeElement as HTMLElement;
    page.querySelector<HTMLButtonElement>('[aria-label="Compartir ticket"]')?.click();

    await vi.waitFor(() => expect(share).toHaveBeenCalledOnce());

    const shareData = share.mock.calls[0][0] as ShareData;
    const [sharedFile] = shareData.files ?? [];

    expect(Object.keys(shareData)).toEqual(['files']);
    expect(canShare).toHaveBeenCalledWith({ files: [sharedFile] });
    expect(sharedFile).toBeInstanceOf(File);
    expect(sharedFile.name).toBe('tickets_1.pdf');
    expect(sharedFile.type).toBe('application/pdf');
    expect(sharedFile.size).toBeGreaterThan(0);

    vi.unstubAllGlobals();
  });

  it('shows a loading overlay and disables PDF actions while the file is generated', async () => {
    let resolvePdf!: (pdf: Blob) => void;
    const downloadPdf = vi.fn(
      () =>
        new Promise<Blob>((resolve) => {
          resolvePdf = resolve;
        }),
    );
    const createObjectUrl = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:ticket');
    const revokeObjectUrl = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => undefined);
    let downloadedFilename = '';
    const linkClick = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(function (
      this: HTMLAnchorElement,
    ) {
      downloadedFilename = this.download;
    });

    await TestBed.configureTestingModule({
      imports: [TicketsPage],
      providers: [{ provide: ToastService, useValue: { danger: vi.fn() } }],
    })
      .overrideComponent(TicketsPage, {
        set: {
          providers: [
            {
              provide: TicketService,
              useValue: withCustomLoading({
                getTickets: () => Promise.resolve([ticket({})]),
                downloadPdf,
              }),
            },
          ],
        },
      })
      .compileComponents();

    const fixture = TestBed.createComponent(TicketsPage);
    fixture.detectChanges();
    await fixture.whenStable();
    fixture.detectChanges();

    const page = fixture.nativeElement as HTMLElement;
    page.querySelector<HTMLButtonElement>('[aria-label="Descargar ticket"]')?.click();
    fixture.detectChanges();

    expect(page.querySelector('.tickets-list__generating')?.textContent).toContain(
      'Generando PDF...',
    );
    expect(page.querySelector('.tickets-list')?.getAttribute('aria-busy')).toBe('true');
    expect(page.querySelector<HTMLButtonElement>('[aria-label="Descargar ticket"]')?.disabled).toBe(
      true,
    );
    expect(page.querySelector<HTMLButtonElement>('[aria-label="Compartir ticket"]')?.disabled).toBe(
      true,
    );

    resolvePdf(new Blob(['pdf'], { type: 'application/pdf' }));
    await fixture.whenStable();
    await vi.waitFor(() => expect(downloadedFilename).toBe('tickets_1.pdf'));
    fixture.detectChanges();

    expect(page.querySelector('.tickets-list__generating')).toBeNull();
    expect(page.querySelector('.tickets-list')?.getAttribute('aria-busy')).toBe('false');

    createObjectUrl.mockRestore();
    revokeObjectUrl.mockRestore();
    linkClick.mockRestore();
  });

  it('hides the active tickets header when there are no available tickets', async () => {
    await TestBed.configureTestingModule({
      imports: [TicketsPage],
      providers: [{ provide: ToastService, useValue: { danger: vi.fn() } }],
    })
      .overrideComponent(TicketsPage, {
        set: {
          providers: [
            {
              provide: TicketService,
              useValue: withCustomLoading({ getTickets: () => Promise.resolve([]) }),
            },
          ],
        },
      })
      .compileComponents();

    const fixture = TestBed.createComponent(TicketsPage);
    fixture.detectChanges();
    await fixture.whenStable();
    fixture.detectChanges();

    const page = fixture.nativeElement as HTMLElement;

    expect(page.querySelector('.tickets-list__select-all')).toBeNull();
    expect(page.querySelector('.tickets-list__bulk-actions')).toBeNull();
    expect(page.textContent).not.toContain('TICKETS ACTIVOS');
    expect(page.textContent).toContain('Aún no tenés tickets disponibles.');
  });

  it('separates expired and used tickets into the disabled inactive list', async () => {
    const tickets = [
      ticket({ id: 1 }),
      ticket({ id: 2, is_valid: false, is_expired: true }),
      ticket({ id: 3, is_valid: false, is_used: true, used_at: '2026-10-10T00:00:00.000Z' }),
    ];

    await TestBed.configureTestingModule({
      imports: [TicketsPage],
      providers: [{ provide: ToastService, useValue: { danger: vi.fn() } }],
    })
      .overrideComponent(TicketsPage, {
        set: {
          providers: [
            {
              provide: TicketService,
              useValue: withCustomLoading({ getTickets: () => Promise.resolve(tickets) }),
            },
          ],
        },
      })
      .compileComponents();

    const fixture = TestBed.createComponent(TicketsPage);
    fixture.detectChanges();
    await fixture.whenStable();
    fixture.detectChanges();

    const ticketComponents = fixture.debugElement.queryAll(By.directive(TicketComponent));
    const [activeTicket, expiredTicket, usedTicket] = ticketComponents.map(
      (element) => element.componentInstance as TicketComponent,
    );

    expect(ticketComponents).toHaveLength(3);
    expect(activeTicket.disabled()).toBe(false);
    expect(expiredTicket.disabled()).toBe(true);
    expect(expiredTicket.expired()).toBe(true);
    expect(usedTicket.disabled()).toBe(true);
    expect(usedTicket.expired()).toBe(false);
    expect(fixture.nativeElement.textContent).toContain('TICKETS INACTIVOS');

    const page = fixture.nativeElement as HTMLElement;
    const inactiveElements = Array.from(
      page.querySelectorAll<HTMLElement>('.tickets-list__inactive app-ticket'),
    );
    expect(inactiveElements).toHaveLength(2);
    inactiveElements.forEach((element) => {
      expect(element.querySelector('.ticket--disabled')).not.toBeNull();
      expect(element.querySelector<HTMLButtonElement>('app-button button')?.disabled).toBe(true);
      expect(element.querySelector('input[type="checkbox"]')).toBeNull();
      expect(element.querySelector('[aria-label="Descargar ticket"]')).toBeNull();
      expect(element.querySelector('[aria-label="Compartir ticket"]')).toBeNull();
    });
  });

  it('opens the QR modal with the selected ticket data', async () => {
    const modalService = { openQr: vi.fn() };

    await TestBed.configureTestingModule({
      imports: [TicketsPage],
      providers: [
        { provide: ToastService, useValue: { danger: vi.fn() } },
        { provide: ModalService, useValue: modalService },
      ],
    })
      .overrideComponent(TicketsPage, {
        set: {
          providers: [
            {
              provide: TicketService,
              useValue: withCustomLoading({
                getTickets: () => Promise.resolve([ticket({})]),
              }),
            },
          ],
        },
      })
      .compileComponents();

    const fixture = TestBed.createComponent(TicketsPage);
    fixture.detectChanges();
    await fixture.whenStable();
    fixture.detectChanges();

    const page = fixture.nativeElement as HTMLElement;
    page.querySelector<HTMLButtonElement>('app-ticket app-button button')?.click();

    expect(modalService.openQr).toHaveBeenCalledWith({
      title: 'Entrada general',
      id: 1,
      date: '09-octubre',
      ticket: 'ticket-1',
    });
  });
});
