import { provideHttpClient } from '@angular/common/http';
import {
  HttpTestingController,
  provideHttpClientTesting,
} from '@angular/common/http/testing';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { By } from '@angular/platform-browser';
import { TestBed } from '@angular/core/testing';
import { MissionSchema, MissionStateResponse, SurvivorSchema } from '@fog-explorer/api-interfaces';
import { EBS_BASE_URL } from '../ebs/ebs-api.service';
import { TwitchAuthService } from '../twitch/twitch-auth.service';
import { AmbientEventData } from './ambient-event.component';
import { PanelShellComponent } from './panel-shell.component';

// Stub child components using @Input()/@Output() — JIT doesn't recognise signal inputs
@Component({ selector: 'app-minimised-panel', standalone: true, template: '' })
class MinimisedPanelStub {
  @Input() missionState!: NonNullable<MissionStateResponse>;
  @Output() lanternClick = new EventEmitter<void>();
}

@Component({ selector: 'app-ambient-event', standalone: true, template: '' })
class AmbientEventStub {
  @Input() event!: AmbientEventData;
  @Output() dismissed = new EventEmitter<void>();
}

@Component({ selector: 'app-expanded-panel', standalone: true, template: '' })
class ExpandedPanelStub {
  @Input() missionState!: NonNullable<MissionStateResponse>;
  @Output() panelClose = new EventEmitter<void>();
}

const BASE = 'https://test.local';
const STATE_URL = `${BASE}/missions/state`;

function makeJwt(): string {
  const payload = btoa(
    JSON.stringify({ opaque_user_id: 'U123', channel_id: 'ch1', role: 'viewer', exp: 9999999999 })
  ).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
  return `eyJhbGciOiJIUzI1NiJ9.${payload}.sig`;
}

function makeAnonymousJwt(): string {
  const payload = btoa(
    JSON.stringify({ opaque_user_id: 'Aanon', channel_id: 'ch1', role: 'viewer', exp: 9999999999 })
  ).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
  return `eyJhbGciOiJIUzI1NiJ9.${payload}.sig`;
}

const SURVIVOR = SurvivorSchema.parse({
  id: 'a1b2c3d4-e5f6-4789-a012-b3c4d5e6f701',
  opaqueUserId: 'U123',
  channelId: 'ch1',
  name: 'Hana',
  state: 'active',
  stats: { objectives: 5, survival: 5, altruism: 5 },
  perkSlots: [],
  createdAt: new Date().toISOString(),
});

const MISSION = MissionSchema.parse({
  id: 'b2c3d4e5-f6a7-4890-b123-c4d5e6f7a801',
  groupId: null,
  participants: [{ survivorId: SURVIVOR.id, state: 'active', hookCount: 0 }],
  difficulty: 2,
  status: 'active',
  encounterLibraryVersion: '1.0.0',
  nextTickAt: new Date(Date.now() + 60000).toISOString(),
  tickIndex: 3,
  startedAt: new Date().toISOString(),
  endedAt: null,
});

const MISSION_STATE = { mission: MISSION, survivors: [SURVIVOR], recentLog: [] };

@Component({
  standalone: true,
  imports: [PanelShellComponent],
  template: `<app-panel-shell />`,
})
class HostComponent {}

describe('PanelShellComponent', () => {
  let controller: HttpTestingController;
  let authService: TwitchAuthService;
  let visibilityCallback: ((v: boolean, ctx: TwitchContext) => void) | undefined;

  function mountTwitchExt(jwt = makeJwt()) {
    (window as Window & { Twitch?: unknown }).Twitch = {
      ext: {
        onAuthorized: (cb: (a: TwitchAuth) => void) =>
          cb({ channelId: 'ch1', clientId: 'c1', token: jwt, userId: 'U123' }),
        onContext: () => undefined,
        onVisibilityChanged: (cb: (v: boolean, ctx: TwitchContext) => void) => {
          visibilityCallback = cb;
        },
        listen: () => undefined,
        unlisten: () => undefined,
        send: () => undefined,
      },
    };
  }

  function createComponent() {
    const fixture = TestBed.createComponent(HostComponent);
    fixture.detectChanges();
    return fixture;
  }

  beforeEach(() => {
    visibilityCallback = undefined;
    TestBed.configureTestingModule({
      providers: [
        provideHttpClient(),
        provideHttpClientTesting(),
        { provide: EBS_BASE_URL, useValue: BASE },
      ],
    });
    TestBed.overrideComponent(PanelShellComponent, {
      set: {
        imports: [MinimisedPanelStub, AmbientEventStub, ExpandedPanelStub],
      },
    });
    controller = TestBed.inject(HttpTestingController);
    authService = TestBed.inject(TwitchAuthService);
  });

  afterEach(() => {
    controller.verify();
    delete (window as Window & { Twitch?: unknown }).Twitch;
    vi.useRealTimers();
  });

  it('renders nothing before onAuthorized fires', () => {
    (window as Window & { Twitch?: unknown }).Twitch = {
      ext: {
        onAuthorized: () => undefined,
        onContext: () => undefined,
        onVisibilityChanged: (cb: (v: boolean, ctx: TwitchContext) => void) => { visibilityCallback = cb; },
        listen: () => undefined,
        unlisten: () => undefined,
        send: () => undefined,
      },
    };
    const fixture = createComponent();
    // Angular @if leaves comment nodes; check no element children are rendered
    const shell = fixture.nativeElement.querySelector('app-panel-shell') as HTMLElement;
    expect(shell.querySelectorAll('*').length).toBe(0);
  });

  it('shows onboarding when authorized but no active mission', () => {
    mountTwitchExt();
    const fixture = createComponent();

    controller.expectOne(STATE_URL).flush(null);
    fixture.detectChanges();

    expect(fixture.nativeElement.textContent).toContain('The fog stirs');
  });

  it('shows anonymous panel for A-prefixed opaque_user_id', () => {
    mountTwitchExt(makeAnonymousJwt());
    const fixture = createComponent();

    controller.expectOne(STATE_URL).flush(null);
    fixture.detectChanges();

    expect(fixture.nativeElement.querySelector('.anon-panel')).not.toBeNull();
  });

  it('shows minimised panel when mission state is available', () => {
    mountTwitchExt();
    const fixture = createComponent();

    controller.expectOne(STATE_URL).flush(MISSION_STATE);
    fixture.detectChanges();

    expect(fixture.nativeElement.querySelector('app-minimised-panel')).not.toBeNull();
  });

  it('toggles to expanded on lantern click and back on close', () => {
    mountTwitchExt();
    const fixture = createComponent();
    controller.expectOne(STATE_URL).flush(MISSION_STATE);
    fixture.detectChanges();

    // Emit lanternClick from the stub component
    fixture.debugElement.query(By.directive(MinimisedPanelStub))
      .componentInstance.lanternClick.emit();
    fixture.detectChanges();
    expect(fixture.nativeElement.querySelector('app-expanded-panel')).not.toBeNull();

    // Emit close from the expanded stub
    fixture.debugElement.query(By.directive(ExpandedPanelStub))
      .componentInstance.panelClose.emit();
    fixture.detectChanges();
    expect(fixture.nativeElement.querySelector('app-minimised-panel')).not.toBeNull();
  });

  it('shows ambient event on injury and auto-dismisses after 4s', async () => {
    vi.useFakeTimers();
    mountTwitchExt();
    const fixture = createComponent();
    controller.expectOne(STATE_URL).flush(MISSION_STATE);
    fixture.detectChanges();

    const injuredState = {
      ...MISSION_STATE,
      survivors: [{ ...SURVIVOR, state: 'injured' as const }],
    };
    controller.expectNone(STATE_URL);

    // Simulate a PubSub tick arriving that updates state to injured
    controller.expectNone(STATE_URL);
    // Directly set state by triggering a second fetch with an injured survivor
    // (In production this comes from PubSub → store.refresh() → REST)
    // We trigger via store.refresh() here
    TestBed.inject(
      (await import('../mission/mission-state.store')).MissionStateStore
    ).refresh();
    controller.expectOne(STATE_URL).flush(injuredState);
    fixture.detectChanges();

    expect(fixture.nativeElement.querySelector('app-ambient-event')).not.toBeNull();

    await vi.advanceTimersByTimeAsync(4000);
    fixture.detectChanges();
    expect(fixture.nativeElement.querySelector('app-minimised-panel')).not.toBeNull();
  });

  it('drops ambient event when overlay is hidden', async () => {
    mountTwitchExt();
    const fixture = createComponent();
    controller.expectOne(STATE_URL).flush(MISSION_STATE);
    fixture.detectChanges();

    visibilityCallback!(false, {} as TwitchContext);

    const injuredState = {
      ...MISSION_STATE,
      survivors: [{ ...SURVIVOR, state: 'injured' as const }],
    };
    TestBed.inject(
      (await import('../mission/mission-state.store')).MissionStateStore
    ).refresh();
    controller.expectOne(STATE_URL).flush(injuredState);
    fixture.detectChanges();

    // Still minimised — ambient event dropped while hidden
    expect(fixture.nativeElement.querySelector('app-minimised-panel')).not.toBeNull();
  });
});
