import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { provideRouter } from '@angular/router';
import { AUTH_BFF_BASE_URL, type CurrentUser } from 'feature-auth';
import { Header } from './header';

const BFF_BASE = 'http://bff.test/api';
const ME_URL = `${BFF_BASE}/auth/me`;
const CAPABILITIES_URL = `${BFF_BASE}/me/capabilities`;

const USER: CurrentUser = {
  oid: 'user-oid',
  tid: 'tenant-id',
  username: 'jane.doe@apf.example',
  displayName: 'Jane Doe',
};

async function setup(opts?: {
  onBootstrap?: 'authenticated' | 'anonymous' | 'error' | 'leave';
  canAccessAdmin?: boolean;
}) {
  TestBed.configureTestingModule({
    imports: [Header],
    providers: [
      provideRouter([]),
      provideHttpClient(),
      provideHttpClientTesting(),
      { provide: AUTH_BFF_BASE_URL, useValue: BFF_BASE },
    ],
  });
  const fixture = TestBed.createComponent(Header);
  const httpCtrl = TestBed.inject(HttpTestingController);
  // The bootstrap /me fires synchronously in AuthService's
  // constructor — match and resolve it according to the test's
  // intent. `leave` keeps the loading state visible.
  const intent = opts?.onBootstrap ?? 'authenticated';
  if (intent !== 'leave') {
    const req = httpCtrl.expectOne(ME_URL);
    if (intent === 'authenticated') {
      req.flush(USER);
      // Multiple change-detection rounds + microtasks let Angular's
      // signal scheduler dispatch the `effect()` in
      // `CapabilitiesService` so its GET request is queued before
      // we look it up below.
      await flushPendingEffects(fixture);
      httpCtrl.expectOne(CAPABILITIES_URL).flush({
        canAccessAdmin: opts?.canAccessAdmin ?? false,
      });
    } else if (intent === 'anonymous') {
      req.flush({ error: 'unauthenticated' }, { status: 401, statusText: 'Unauthorized' });
    } else {
      req.flush('boom', { status: 500, statusText: 'Internal Server Error' });
    }
    await Promise.resolve();
  }
  await flushPendingEffects(fixture);
  return { fixture, httpCtrl };
}

async function flushPendingEffects(fixture: ReturnType<typeof TestBed.createComponent>) {
  for (let i = 0; i < 4; i++) {
    fixture.detectChanges();
    await fixture.whenStable();
    await Promise.resolve();
  }
}

describe('Header', () => {
  beforeEach(() => {
    localStorage.clear();
    TestBed.resetTestingModule();
  });

  it('renders the brand link to /', async () => {
    const { fixture } = await setup({ onBootstrap: 'anonymous' });
    const link = fixture.nativeElement.querySelector('a[href="/"]') as HTMLAnchorElement | null;
    expect(link).not.toBeNull();
    expect(link?.textContent?.trim()).toContain('APF Portal');
  });

  it('exposes a primary navigation landmark', async () => {
    const { fixture } = await setup({ onBootstrap: 'anonymous' });
    const nav = fixture.nativeElement.querySelector('nav[aria-label="Primary"]');
    expect(nav).not.toBeNull();
  });

  it('exposes a search landmark with a labelled input', async () => {
    const { fixture } = await setup({ onBootstrap: 'anonymous' });
    const root = fixture.nativeElement as HTMLElement;
    expect(root.querySelector('form[role="search"]')).not.toBeNull();
    const input = root.querySelector('input#global-search') as HTMLInputElement | null;
    expect(input).not.toBeNull();
    expect(input?.type).toBe('search');
    const label = root.querySelector('label[for="global-search"]');
    expect(label).not.toBeNull();
  });

  it('renders the notifications / help / settings action buttons', async () => {
    const { fixture } = await setup({ onBootstrap: 'anonymous' });
    const root = fixture.nativeElement as HTMLElement;
    expect(root.querySelector('button[aria-label="Notifications"]')).not.toBeNull();
    expect(root.querySelector('button[aria-label="Help"]')).not.toBeNull();
    expect(root.querySelector('button[aria-label="Settings"]')).not.toBeNull();
  });

  it('embeds the theme switcher', async () => {
    const { fixture } = await setup({ onBootstrap: 'anonymous' });
    expect(fixture.nativeElement.querySelector('app-theme-switcher')).not.toBeNull();
  });

  it('renders the logo zone, expanded with wordmark by default', async () => {
    const { fixture } = await setup({ onBootstrap: 'anonymous' });
    const root = fixture.nativeElement as HTMLElement;
    const zone = root.querySelector('.header__logo-zone');
    expect(zone).not.toBeNull();
    expect(zone?.classList.contains('header__logo-zone--collapsed')).toBe(false);
    expect(root.textContent ?? '').toContain('APF Portal');
  });

  it('collapses the logo zone and hides the wordmark when the sidebar is collapsed', async () => {
    localStorage.setItem('portal-shell:sidebar-collapsed', '1');
    const { fixture } = await setup({ onBootstrap: 'anonymous' });
    const root = fixture.nativeElement as HTMLElement;
    const zone = root.querySelector('.header__logo-zone');
    expect(zone?.classList.contains('header__logo-zone--collapsed')).toBe(true);
    // The collapsed brand link contains the logo image only — no
    // wordmark span. We assert by inspecting the link's direct
    // children.
    const wordmarkSpan = root.querySelector('a[href="/"] > span');
    expect(wordmarkSpan).toBeNull();
  });

  describe('user widget', () => {
    it('renders a Sign in button when the BFF says anonymous', async () => {
      const { fixture } = await setup({ onBootstrap: 'anonymous' });
      const root = fixture.nativeElement as HTMLElement;
      const btn = root.querySelector('[data-testid="sign-in-button"]') as HTMLButtonElement | null;
      expect(btn).not.toBeNull();
      expect(btn?.textContent?.trim()).toBe('Sign in');
      expect(root.querySelector('[data-testid="user-avatar"]')).toBeNull();
    });

    it('renders the user menu trigger with initials when authenticated', async () => {
      const { fixture } = await setup({ onBootstrap: 'authenticated' });
      const root = fixture.nativeElement as HTMLElement;
      // The avatar + dropdown live inside the shared `lib-user-menu`;
      // we assert at the trigger boundary and let the shared spec
      // cover panel internals (UserMenu.spec.ts).
      const menu = root.querySelector('[data-testid="user-menu"]');
      expect(menu).not.toBeNull();
      const avatar = menu?.querySelector('.user-menu__avatar');
      expect(avatar?.textContent?.trim()).toBe('JD');
      expect(root.querySelector('[data-testid="sign-in-button"]')).toBeNull();
    });

    it('opens the menu and exposes displayName + Sign out inside the panel', async () => {
      const { fixture } = await setup({ onBootstrap: 'authenticated' });
      const trigger = (fixture.nativeElement as HTMLElement).querySelector<HTMLButtonElement>(
        '[data-testid="user-menu"] button[aria-haspopup="menu"]',
      );
      trigger?.click();
      fixture.detectChanges();
      await fixture.whenStable();
      const panel = document.body.querySelector('.user-menu__panel');
      expect(panel).not.toBeNull();
      expect(panel?.querySelector('.user-menu__header-name')?.textContent).toContain(
        USER.displayName,
      );
      expect(panel?.querySelector('.user-menu__item--sign-out')).not.toBeNull();
    });

    it('omits the "Open Portal Admin" entry when canAccessAdmin is false', async () => {
      const { fixture } = await setup({ onBootstrap: 'authenticated', canAccessAdmin: false });
      const trigger = (fixture.nativeElement as HTMLElement).querySelector<HTMLButtonElement>(
        '[data-testid="user-menu"] button[aria-haspopup="menu"]',
      );
      trigger?.click();
      fixture.detectChanges();
      await fixture.whenStable();
      const items = Array.from(
        document.body.querySelectorAll<HTMLElement>('.user-menu__panel .user-menu__item'),
      );
      expect(items.some((el) => el.textContent?.includes('Open Portal Admin'))).toBe(false);
    });

    it('includes the "Open Portal Admin" entry pointing at the admin app when canAccessAdmin is true', async () => {
      const { fixture } = await setup({ onBootstrap: 'authenticated', canAccessAdmin: true });
      const trigger = (fixture.nativeElement as HTMLElement).querySelector<HTMLButtonElement>(
        '[data-testid="user-menu"] button[aria-haspopup="menu"]',
      );
      trigger?.click();
      fixture.detectChanges();
      await fixture.whenStable();
      const adminLink = document.body.querySelector<HTMLAnchorElement>(
        '.user-menu__panel a.user-menu__item[href^="http"]',
      );
      expect(adminLink).not.toBeNull();
      expect(adminLink?.textContent).toContain('Open Portal Admin');
    });

    it('shows the error chip when /me fails with a non-401', async () => {
      const { fixture } = await setup({ onBootstrap: 'error' });
      const root = fixture.nativeElement as HTMLElement;
      const chip = root.querySelector('[data-testid="auth-error"]');
      expect(chip).not.toBeNull();
      expect(chip?.getAttribute('role')).toBe('status');
    });

    it('shows the loading placeholder before /me resolves', async () => {
      const { fixture, httpCtrl } = await setup({ onBootstrap: 'leave' });
      const root = fixture.nativeElement as HTMLElement;
      expect(root.querySelector('[data-testid="sign-in-button"]')).toBeNull();
      expect(root.querySelector('[data-testid="user-menu"]')).toBeNull();
      // Drain the still-pending request so the controller stays clean.
      httpCtrl.expectOne(ME_URL).flush({}, { status: 401, statusText: 'Unauthorized' });
    });
  });
});
