import { describe, it, expect, afterEach, vi, type Mock } from 'vitest';
import { TestBed } from '@angular/core/testing';
import { signal } from '@angular/core';
import { ChatbotHost } from './chatbot-host';
import { ChatbotService } from './chatbot.service';
import type { ChatbotMessage, ChatbotView, CitationRef } from './chatbot.types';

/**
 * Component-level tests for `ChatbotHost`. The service is mocked so
 * the spec drives the visual states by setting signals directly
 * rather than running the SSE stream.
 *
 * Focus areas:
 *   - role / aria attributes on the launcher, the panel, the
 *     message log, and the citation side panel (ADR-0016 contract).
 *   - Empty-state suggestion buttons emit `send()` with the right
 *     prompt.
 *   - Input handling: Enter submits, Shift+Enter inserts newline,
 *     submit is disabled while streaming and re-enabled after.
 */

interface FakeService {
  view: ReturnType<typeof signal<ChatbotView>>;
  messages: ReturnType<typeof signal<readonly ChatbotMessage[]>>;
  selectedCitation: ReturnType<typeof signal<CitationRef | null>>;
  streaming: ReturnType<typeof signal<boolean>>;
  isOpen: () => boolean;
  isFullscreen: () => boolean;
  isStreaming: () => boolean;
  isEmpty: () => boolean;
  open: Mock<() => void>;
  close: Mock<() => void>;
  toggleFullscreen: Mock<() => void>;
  stop: Mock<() => void>;
  selectCitation: Mock<(c: CitationRef | null) => void>;
  send: Mock<(prompt: string) => Promise<void>>;
}

function makeFakeService(): FakeService {
  const view = signal<ChatbotView>('closed');
  const messages = signal<readonly ChatbotMessage[]>([]);
  const selectedCitation = signal<CitationRef | null>(null);
  const streaming = signal<boolean>(false);

  return {
    view,
    messages,
    selectedCitation,
    streaming,
    isOpen: () => view() !== 'closed',
    isFullscreen: () => view() === 'fullscreen',
    isStreaming: () => streaming(),
    isEmpty: () => messages().length === 0,
    open: vi.fn<() => void>(() => view.set('windowed')),
    close: vi.fn<() => void>(() => view.set('closed')),
    toggleFullscreen: vi.fn<() => void>(() =>
      view.update((v) => (v === 'fullscreen' ? 'windowed' : 'fullscreen')),
    ),
    stop: vi.fn<() => void>(() => streaming.set(false)),
    selectCitation: vi.fn<(c: CitationRef | null) => void>((c) => selectedCitation.set(c)),
    send: vi.fn<(prompt: string) => Promise<void>>(),
  };
}

function setup(): {
  fixture: ReturnType<typeof TestBed.createComponent<ChatbotHost>>;
  api: FakeService;
} {
  const api = makeFakeService();
  TestBed.configureTestingModule({
    imports: [ChatbotHost],
    providers: [{ provide: ChatbotService, useValue: api }],
  });
  const fixture = TestBed.createComponent(ChatbotHost);
  fixture.detectChanges();
  return { fixture, api };
}

describe('ChatbotHost — launcher', () => {
  afterEach(() => TestBed.resetTestingModule());

  it('renders an accessible launcher with aria-expanded=false when closed', () => {
    const { fixture } = setup();
    const launcher = fixture.nativeElement.querySelector(
      'button.chatbot-launcher',
    ) as HTMLButtonElement;
    expect(launcher).not.toBeNull();
    expect(launcher.getAttribute('aria-label')).toBeTruthy();
    expect(launcher.getAttribute('aria-expanded')).toBe('false');
  });

  it('flips aria-expanded to true when the panel opens', () => {
    const { fixture, api } = setup();
    api.open();
    fixture.detectChanges();
    const launcher = fixture.nativeElement.querySelector(
      'button.chatbot-launcher',
    ) as HTMLButtonElement;
    expect(launcher.getAttribute('aria-expanded')).toBe('true');
  });

  it('clicking the launcher calls service.open()', () => {
    const { fixture, api } = setup();
    const launcher = fixture.nativeElement.querySelector(
      'button.chatbot-launcher',
    ) as HTMLButtonElement;
    launcher.click();
    expect(api.open).toHaveBeenCalled();
  });
});

describe('ChatbotHost — panel chrome', () => {
  afterEach(() => TestBed.resetTestingModule());

  it('panel exposes role=dialog + aria-modal=false + aria-labelledby', () => {
    const { fixture, api } = setup();
    api.open();
    fixture.detectChanges();
    const panel = fixture.nativeElement.querySelector('.chatbot-panel') as HTMLElement;
    expect(panel.getAttribute('role')).toBe('dialog');
    expect(panel.getAttribute('aria-modal')).toBe('false');
    const labelId = panel.getAttribute('aria-labelledby');
    expect(labelId).toBeTruthy();
    const title = fixture.nativeElement.querySelector(`#${labelId}`);
    expect(title?.textContent?.trim()).toBeTruthy();
  });

  it('renders fullscreen modifier when the view is fullscreen', () => {
    const { fixture, api } = setup();
    api.view.set('fullscreen');
    fixture.detectChanges();
    const panel = fixture.nativeElement.querySelector('.chatbot-panel') as HTMLElement;
    expect(panel.classList.contains('chatbot-panel--fullscreen')).toBe(true);
  });

  it('Escape key on the panel triggers service.close()', () => {
    const { fixture, api } = setup();
    api.open();
    fixture.detectChanges();
    const panel = fixture.nativeElement.querySelector('.chatbot-panel') as HTMLElement;
    panel.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
    expect(api.close).toHaveBeenCalled();
  });
});

describe('ChatbotHost — empty state suggestions', () => {
  afterEach(() => TestBed.resetTestingModule());

  it('renders four suggestion buttons in the empty state', () => {
    const { fixture, api } = setup();
    api.open();
    fixture.detectChanges();
    const suggestions = fixture.nativeElement.querySelectorAll('button.chatbot-suggestion');
    expect(suggestions.length).toBe(4);
  });

  it('clicking a suggestion calls send() with the suggestion label', () => {
    const { fixture, api } = setup();
    api.open();
    fixture.detectChanges();
    const suggestion = fixture.nativeElement.querySelector(
      'button.chatbot-suggestion',
    ) as HTMLButtonElement;
    const label = suggestion.textContent?.trim();
    suggestion.click();
    expect(api.send).toHaveBeenCalledWith(label);
  });
});

describe('ChatbotHost — message rendering + log live region', () => {
  afterEach(() => TestBed.resetTestingModule());

  it('renders messages inside a role="log" aria-live="polite" container', () => {
    const { fixture, api } = setup();
    api.open();
    api.messages.set([
      { id: 'm1', role: 'user', content: 'hi', citations: [], done: true },
      {
        id: 'm2',
        role: 'assistant',
        content: 'hello',
        citations: [],
        done: false,
      },
    ]);
    fixture.detectChanges();
    const log = fixture.nativeElement.querySelector('div.chatbot-messages') as HTMLElement;
    expect(log.getAttribute('role')).toBe('log');
    expect(log.getAttribute('aria-live')).toBe('polite');
    // Each message is an <article> child so screen readers can
    // navigate them as discrete content pieces.
    const articles = log.querySelectorAll('article.chatbot-message');
    expect(articles.length).toBe(2);
  });

  it('shows typing-dot animation only on the streaming assistant message', () => {
    const { fixture, api } = setup();
    api.open();
    api.messages.set([
      { id: 'm1', role: 'user', content: 'hi', citations: [], done: true },
      {
        id: 'm2',
        role: 'assistant',
        content: 'streaming...',
        citations: [],
        done: false,
      },
    ]);
    fixture.detectChanges();
    const typing = fixture.nativeElement.querySelectorAll('.chatbot-typing');
    expect(typing.length).toBe(1);
  });

  it('renders an inline alert when a message carries an error', () => {
    const { fixture, api } = setup();
    api.open();
    api.messages.set([
      {
        id: 'm1',
        role: 'assistant',
        content: '',
        citations: [],
        done: true,
        errorCode: 'urn:apf-ai:unavailable',
        errorMessage: 'Service unavailable',
      },
    ]);
    fixture.detectChanges();
    const alert = fixture.nativeElement.querySelector('[role="alert"]') as HTMLElement;
    expect(alert).not.toBeNull();
    expect(alert.textContent?.trim()).toBe('Service unavailable');
  });
});

describe('ChatbotHost — input form', () => {
  afterEach(() => TestBed.resetTestingModule());

  it('Enter submits, send() is called with the textarea value', () => {
    const { fixture, api } = setup();
    api.open();
    fixture.detectChanges();
    const textarea = fixture.nativeElement.querySelector(
      'textarea.chatbot-panel__input',
    ) as HTMLTextAreaElement;
    textarea.value = 'hello';
    textarea.dispatchEvent(new Event('input', { bubbles: true }));
    fixture.detectChanges();
    textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
    expect(api.send).toHaveBeenCalledWith('hello');
  });

  it('the send button is disabled while the draft is empty', () => {
    const { fixture, api } = setup();
    api.open();
    fixture.detectChanges();
    const send = fixture.nativeElement.querySelector(
      'button.chatbot-action--send',
    ) as HTMLButtonElement;
    expect(send.disabled).toBe(true);
  });

  it('renders a Stop button while streaming and calls service.stop() on click', () => {
    const { fixture, api } = setup();
    api.open();
    api.streaming.set(true);
    fixture.detectChanges();
    const stop = fixture.nativeElement.querySelector(
      'button.chatbot-action--stop',
    ) as HTMLButtonElement;
    expect(stop).not.toBeNull();
    stop.click();
    expect(api.stop).toHaveBeenCalled();
  });
});

describe('ChatbotHost — citation side panel', () => {
  afterEach(() => TestBed.resetTestingModule());

  const CITATION: CitationRef = {
    index: 1,
    chunkId: 'c-1',
    documentId: 'd-1',
    source: 'doc.pdf',
    score: 0.87,
    snippet: 'Lorem ipsum dolor sit amet.',
  };

  it('clicking a citation chip calls service.selectCitation(citation)', () => {
    const { fixture, api } = setup();
    api.open();
    api.messages.set([
      {
        id: 'm1',
        role: 'assistant',
        content: 'See',
        citations: [CITATION],
        done: true,
      },
    ]);
    fixture.detectChanges();
    const chip = fixture.nativeElement.querySelector(
      'button.chatbot-citation',
    ) as HTMLButtonElement;
    chip.click();
    expect(api.selectCitation).toHaveBeenCalledWith(CITATION);
  });

  it('renders the citation panel when selectedCitation is set', () => {
    const { fixture, api } = setup();
    api.selectedCitation.set(CITATION);
    fixture.detectChanges();
    const panel = fixture.nativeElement.querySelector('.chatbot-citation-panel') as HTMLElement;
    expect(panel).not.toBeNull();
    expect(panel.textContent).toContain('doc.pdf');
    expect(panel.textContent).toContain('0.87');
    expect(panel.textContent).toContain('Lorem ipsum');
  });
});
