import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { TestBed } from '@angular/core/testing';
import { AUTH_BFF_BASE_URL, AUTH_CSRF_COOKIE_NAME } from 'feature-auth';
import { ChatbotApiError, ChatbotApiService } from './chatbot-api.service';

const BFF_BASE = 'http://bff.test/api';
const CSRF_COOKIE = 'portal_csrf_test';
const CSRF_VALUE = 'csrf-token-fixture';

function sseStream(chunks: ReadonlyArray<string>): ReadableStream<Uint8Array> {
  const encoder = new TextEncoder();
  return new ReadableStream<Uint8Array>({
    start(controller) {
      for (const chunk of chunks) {
        controller.enqueue(encoder.encode(chunk));
      }
      controller.close();
    },
  });
}

function setCsrfCookie(value: string | null): void {
  if (value === null) {
    Object.defineProperty(document, 'cookie', { value: '', configurable: true });
  } else {
    Object.defineProperty(document, 'cookie', {
      value: `${CSRF_COOKIE}=${encodeURIComponent(value)}`,
      configurable: true,
    });
  }
}

function makeService(): ChatbotApiService {
  TestBed.configureTestingModule({
    providers: [
      { provide: AUTH_BFF_BASE_URL, useValue: BFF_BASE },
      { provide: AUTH_CSRF_COOKIE_NAME, useValue: CSRF_COOKIE },
    ],
  });
  return TestBed.inject(ChatbotApiService);
}

describe('ChatbotApiService', () => {
  const originalFetch = globalThis.fetch;

  beforeEach(() => {
    setCsrfCookie(CSRF_VALUE);
  });

  afterEach(() => {
    globalThis.fetch = originalFetch;
    setCsrfCookie(null);
    TestBed.resetTestingModule();
  });

  it('POSTs to /ai/chat with the CSRF header and credentials included', async () => {
    const fetchSpy = vi.fn<typeof globalThis.fetch>().mockResolvedValue(
      new Response(sseStream(['event: done\ndata: {}\n\n']), {
        status: 200,
        headers: { 'Content-Type': 'text/event-stream' },
      }),
    );
    globalThis.fetch = fetchSpy as unknown as typeof globalThis.fetch;

    const service = makeService();
    const abort = new AbortController();
    await service.openChatStream({ messages: [{ role: 'user', content: 'hi' }] }, abort.signal);

    expect(fetchSpy).toHaveBeenCalledTimes(1);
    const [url, init] = fetchSpy.mock.calls[0]!;
    expect(url).toBe(`${BFF_BASE}/ai/chat`);
    expect(init?.method).toBe('POST');
    expect(init?.credentials).toBe('include');
    const headers = init?.headers as Record<string, string>;
    expect(headers['Content-Type']).toBe('application/json');
    expect(headers['Accept']).toBe('text/event-stream');
    expect(headers['X-CSRF-Token']).toBe(CSRF_VALUE);
    expect(JSON.parse(init?.body as string)).toEqual({
      messages: [{ role: 'user', content: 'hi' }],
    });
  });

  it('yields parsed SSE frames from the response body', async () => {
    globalThis.fetch = vi
      .fn<typeof globalThis.fetch>()
      .mockResolvedValue(
        new Response(
          sseStream(['event: token\ndata: {"value":"a"}\n\nevent: done\ndata: {}\n\n']),
          { status: 200 },
        ),
      ) as unknown as typeof globalThis.fetch;

    const service = makeService();
    const stream = await service.openChatStream(
      { messages: [{ role: 'user', content: 'hi' }] },
      new AbortController().signal,
    );

    const frames = [] as Array<{ event: string; data: unknown }>;
    for await (const frame of stream) {
      frames.push(frame);
    }
    expect(frames).toEqual([
      { event: 'token', data: { value: 'a' } },
      { event: 'done', data: {} },
    ]);
  });

  it('throws ChatbotApiError(csrf-cookie-missing) when no CSRF cookie is set', async () => {
    setCsrfCookie(null);
    const service = makeService();
    await expect(
      service.openChatStream(
        { messages: [{ role: 'user', content: 'hi' }] },
        new AbortController().signal,
      ),
    ).rejects.toMatchObject({
      name: 'ChatbotApiError',
      code: 'csrf-cookie-missing',
    });
  });

  it('throws ChatbotApiError(unauthenticated) on HTTP 401', async () => {
    globalThis.fetch = vi
      .fn<typeof globalThis.fetch>()
      .mockResolvedValue(new Response(null, { status: 401 })) as unknown as typeof globalThis.fetch;

    const service = makeService();
    await expect(
      service.openChatStream(
        { messages: [{ role: 'user', content: 'hi' }] },
        new AbortController().signal,
      ),
    ).rejects.toBeInstanceOf(ChatbotApiError);
    await expect(
      service.openChatStream(
        { messages: [{ role: 'user', content: 'hi' }] },
        new AbortController().signal,
      ),
    ).rejects.toMatchObject({ code: 'unauthenticated', httpStatus: 401 });
  });

  it('forwards the AbortSignal to fetch', async () => {
    const fetchSpy = vi
      .fn<typeof globalThis.fetch>()
      .mockResolvedValue(new Response(sseStream(['event: done\ndata: {}\n\n']), { status: 200 }));
    globalThis.fetch = fetchSpy as unknown as typeof globalThis.fetch;

    const service = makeService();
    const abort = new AbortController();
    await service.openChatStream({ messages: [{ role: 'user', content: 'hi' }] }, abort.signal);

    const [, init] = fetchSpy.mock.calls[0]!;
    expect(init?.signal).toBe(abort.signal);
  });
});
