import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { parseHTML } from 'linkedom';

type ChatModule = {
  ChatPage: {
    render: (el: unknown, client: unknown) => Promise<void>;
    teardown: () => void;
  };
};

function installSelectValueShim(windowObj: any) {
  const proto = windowObj?.HTMLSelectElement?.prototype;
  if (!proto) {return;}
  const descriptor = Object.getOwnPropertyDescriptor(proto, 'value');
  if (descriptor?.set) {return;}
  Object.defineProperty(proto, 'value', {
    configurable: true,
    get() {
      const options = Array.from((this as any).options ?? []) as any[];
      const selected = options.find((option: any) => option.selected);
      return selected ? String(selected.value ?? '') : '';
    },
    set(next) {
      const desired = String(next ?? '');
      const options = Array.from((this as any).options ?? []) as any[];
      let matched = false;
      for (const option of options as any[]) {
        const selected = String(option.value ?? '') === desired;
        option.selected = selected;
        if (selected) {
          matched = true;
        }
      }
      if (!matched && options.length > 0) {
        (options[0] as any).selected = true;
      }
    },
  });
}

function createStream(resultPayload: Record<string, unknown>) {
  const handlers = new Map<string, Array<(data: any) => void>>();
  return {
    on(event: string, cb: (data: any) => void) {
      if (!handlers.has(event)) {
        handlers.set(event, []);
      }
      handlers.get(event)?.push(cb);
    },
    emit(event: string, data: any) {
      for (const cb of handlers.get(event) ?? []) {
        cb(data);
      }
    },
    result: Promise.resolve(resultPayload),
  };
}

function createClient() {
  const calls: Array<{ method: string; params?: Record<string, unknown> }> = [];
  const streamCalls: Array<{ method: string; params?: Record<string, unknown> }> = [];
  const cancelGate: { resolve?: () => void } = {};

  const client = {
    async call(method: string, params?: Record<string, unknown>) {
      calls.push({ method, params });
      if (method === 'sessions.list') {
        return {
          sessions: [
            { id: 'ws:alpha', messageCount: 3, lastMessageAt: Date.now() },
            { id: 'ws:beta', messageCount: 1, lastMessageAt: Date.now() - 5000 },
          ],
        };
      }
      if (method === 'sessions.history') {
        return {
          messages: [
            { role: 'user', content: 'hello', timestamp: Date.now() - 1000 },
            { role: 'assistant', content: 'hi', timestamp: Date.now() },
          ],
        };
      }
      if (method === 'sessions.create') {
        return { sessionId: 'ws:new' };
      }
      if (method === 'agent.cancel') {
        if (cancelGate.resolve) {
          cancelGate.resolve();
        }
        return { ok: true };
      }
      return null;
    },
    stream(method: string, params?: Record<string, unknown>) {
      streamCalls.push({ method, params });
      if (params?.message === 'long run') {
        return {
          on() {},
          result: new Promise((resolve) => {
            cancelGate.resolve = () => resolve({ content: 'cancelled' });
          }),
        };
      }
      return createStream({ content: 'ack' });
    },
  };

  return { client, calls, streamCalls };
}

describe('ChatPage wiring', () => {
  let root: any;
  let windowObj: any;
  let ChatPage: ChatModule['ChatPage'];

  beforeEach(async () => {
    vi.resetModules();
    const { document, window } = parseHTML('<!doctype html><html><body><div id="root"></div></body></html>') as unknown as {
      document: any;
      window: any;
    };
    (globalThis as any).document = document;
    (globalThis as any).window = window;
    (globalThis as any).marked = {
      parse: (text: string) => text,
    };

    root = document.getElementById('root');
    windowObj = window;
    installSelectValueShim(windowObj);

    // @ts-expect-error JS module without declaration file.
    const mod = await import('./chat.js') as unknown as ChatModule;
    ChatPage = mod.ChatPage;
  });

  afterEach(() => {
    ChatPage.teardown();
    delete (globalThis as any).document;
    delete (globalThis as any).window;
    delete (globalThis as any).marked;
  });

  it('wires sessions, history, search mode, send, new session, and cancel', async () => {
    const { client, calls, streamCalls } = createClient();
    await ChatPage.render(root, client);

    expect(calls.some((entry) => entry.method === 'sessions.list')).toBe(true);

    const sortSelect = root.querySelector('#chat-session-sort');
    sortSelect.value = 'name';
    sortSelect.dispatchEvent(new windowObj.Event('change', { bubbles: true }));
    await Promise.resolve();
    expect(calls.filter((entry) => entry.method === 'sessions.list').length).toBeGreaterThanOrEqual(2);

    const sessionSelect = root.querySelector('#chat-session-select');
    const firstSession = sessionSelect.querySelector('option[value="ws:alpha"]');
    if (firstSession) {
      firstSession.selected = true;
    }
    sessionSelect.dispatchEvent(new windowObj.Event('change', { bubbles: true }));

    root.querySelector('#chat-load-history').dispatchEvent(new windowObj.Event('click', { bubbles: true }));
    await Promise.resolve();
    expect(calls.some((entry) => entry.method === 'sessions.history')).toBe(true);

    root.querySelector('#chat-search').dispatchEvent(new windowObj.Event('click', { bubbles: true }));
    const input = root.querySelector('#chat-input');
    input.value = 'status of flynn';
    root.querySelector('#chat-send').dispatchEvent(new windowObj.Event('click', { bubbles: true }));
    await Promise.resolve();

    const searchSend = streamCalls.find((entry) => entry.method === 'agent.send' && entry.params?.message === 'Search the web for: status of flynn');
    expect(searchSend).toBeTruthy();

    root.querySelector('#chat-new-session').dispatchEvent(new windowObj.Event('click', { bubbles: true }));
    await Promise.resolve();
    expect(calls.some((entry) => entry.method === 'sessions.create')).toBe(true);

    input.value = 'long run';
    root.querySelector('#chat-send').dispatchEvent(new windowObj.Event('click', { bubbles: true }));
    await Promise.resolve();
    root.querySelector('#chat-send').dispatchEvent(new windowObj.Event('click', { bubbles: true }));
    await Promise.resolve();

    expect(calls.some((entry) => entry.method === 'agent.cancel')).toBe(true);
  });
});
