import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { AgentBrowserPanel } from '@/pages/Chat/AgentBrowserPanel';
import { deriveAgentBrowserDiagnostics } from '@/lib/agent-browser';
import type {
  AgentBrowserCdpEvent,
  AgentBrowserSnapshot,
} from '../../shared/agent-browser';

const hostApiFetchMock = vi.hoisted(() => vi.fn());
const hostEventListeners = vi.hoisted(
  () => new Map<string, (payload: unknown) => void>(),
);

vi.mock('@/lib/host-api', () => ({
  hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args),
}));

vi.mock('@/lib/host-events', () => ({
  subscribeHostEvent: (
    eventName: string,
    handler: (payload: unknown) => void,
  ) => {
    hostEventListeners.set(eventName, handler);
    return () => {
      hostEventListeners.delete(eventName);
    };
  },
}));

let resizeCallback: ResizeObserverCallback | null = null;

class MockResizeObserver {
  constructor(callback: ResizeObserverCallback) {
    resizeCallback = callback;
  }

  observe() {}

  unobserve() {}

  disconnect() {}
}

function snapshot(
  state: AgentBrowserSnapshot['state'] = 'closed',
  overrides: Partial<AgentBrowserSnapshot> = {},
): AgentBrowserSnapshot {
  const active = state !== 'closed' && state !== 'closing';
  return {
    browserId: active ? 'browser-1' : null,
    projectId: 'project-1',
    projectPath: 'D:/repo',
    state,
    generation: active ? 1 : 0,
    url: active ? 'http://localhost:4173/' : '',
    title: active ? 'Student app' : '',
    visible: active,
    bounds: null,
    canGoBack: false,
    canGoForward: false,
    eventCursor: 0,
    ...overrides,
  };
}

function parseBody(init?: RequestInit): Record<string, unknown> {
  return JSON.parse(String(init?.body ?? '{}')) as Record<string, unknown>;
}

function installBrowserApi(
  initial: AgentBrowserSnapshot,
  events: AgentBrowserCdpEvent[] = [],
) {
  let current = initial;
  let eventsReturned = false;
  hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
    if (path.startsWith('/api/agent-browser/state?')) {
      return { success: true, browser: current };
    }
    if (path === '/api/agent-browser/open') {
      const body = parseBody(init);
      current = snapshot('attached', {
        url: String(body.url),
        visible: true,
        bounds: body.bounds as AgentBrowserSnapshot['bounds'],
      });
      return { success: true, browser: current };
    }
    if (path === '/api/agent-browser/present') {
      const body = parseBody(init);
      current = {
        ...current,
        visible: body.visible === true,
        bounds: (body.bounds as AgentBrowserSnapshot['bounds']) ?? current.bounds,
      };
      return { success: true, browser: current };
    }
    if (path === '/api/agent-browser/navigate') {
      return { success: true, browser: current };
    }
    if (path === '/api/agent-browser/close') {
      current = snapshot();
      return { success: true, browser: current };
    }
    if (path === '/api/agent-browser/cdp/events') {
      const pageEvents = eventsReturned ? [] : events;
      eventsReturned = true;
      return {
        success: true,
        page: {
          events: pageEvents,
          nextCursor: pageEvents.at(-1)?.sequence ?? current.eventCursor,
          hasMore: false,
        },
      };
    }
    throw new Error(`Unexpected Agent Browser request: ${path}`);
  });
}

describe('AgentBrowserPanel', () => {
  beforeEach(() => {
    hostEventListeners.clear();
    resizeCallback = null;
    Object.defineProperty(window, 'ResizeObserver', {
      configurable: true,
      value: MockResizeObserver,
    });
    Object.defineProperty(globalThis, 'ResizeObserver', {
      configurable: true,
      value: MockResizeObserver,
    });
    Object.defineProperty(window, 'devicePixelRatio', {
      configurable: true,
      value: 2,
    });
  });

  it('starts collapsed and cannot open without an active project', () => {
    render(<AgentBrowserPanel projectPath={null} />);

    expect(screen.queryByTestId('agent-browser-panel')).not.toBeInTheDocument();
    expect(screen.getByRole('button', { name: '打开开发浏览器' })).toBeDisabled();
  });

  it('keeps bounded placeholders for oversized Console and Network events', () => {
    const diagnostics = deriveAgentBrowserDiagnostics([
      {
        sequence: 1,
        generation: 1,
        timestamp: 1_000,
        method: 'Runtime.consoleAPICalled',
        payload: {
          kind: 'payload',
          handle: 'console-large',
          byteLength: 70_000,
          contentType: 'application/json',
          expiresAt: '2026-07-28T00:00:00.000Z',
        },
      },
      {
        sequence: 2,
        generation: 1,
        timestamp: 1_010,
        method: 'Network.requestWillBeSent',
        payload: {
          kind: 'payload',
          handle: 'network-large',
          byteLength: 80_000,
          contentType: 'application/json',
          expiresAt: '2026-07-28T00:00:00.000Z',
        },
      },
    ]);

    expect(diagnostics.console[0]?.text).toContain('70000 字节');
    expect(diagnostics.network[0]?.url).toContain('80000 字节');
  });

  it('keeps child-target Network requests separate when request ids collide', () => {
    const diagnostics = deriveAgentBrowserDiagnostics([
      {
        sequence: 1,
        generation: 1,
        timestamp: 1_000,
        method: 'Network.requestWillBeSent',
        params: {
          requestId: 'shared-id',
          request: { method: 'GET', url: 'http://localhost/root' },
        },
      },
      {
        sequence: 2,
        generation: 1,
        timestamp: 1_001,
        method: 'Network.requestWillBeSent',
        sessionRef: 'worker-1',
        params: {
          requestId: 'shared-id',
          request: { method: 'POST', url: 'http://localhost/worker' },
        },
      },
    ]);

    expect(diagnostics.network).toHaveLength(2);
    expect(diagnostics.network.map((entry) => entry.url)).toEqual([
      'http://localhost/root',
      'http://localhost/worker',
    ]);
  });

  it('opens a local URL with viewport bounds expressed in DIP', async () => {
    installBrowserApi(snapshot());
    render(<AgentBrowserPanel projectPath="D:/repo" />);

    fireEvent.click(screen.getByRole('button', { name: '打开开发浏览器' }));
    const viewport = await screen.findByTestId('agent-browser-viewport');
    vi.spyOn(viewport, 'getBoundingClientRect').mockReturnValue({
      x: 120,
      y: 80,
      left: 120,
      top: 80,
      right: 540,
      bottom: 400,
      width: 420,
      height: 320,
      toJSON: () => ({}),
    });

    fireEvent.change(screen.getByRole('textbox', { name: '网页地址' }), {
      target: { value: 'localhost:4173' },
    });
    fireEvent.click(screen.getByRole('button', { name: '打开' }));

    await waitFor(() => {
      const call = hostApiFetchMock.mock.calls.find(
        ([path]) => path === '/api/agent-browser/open',
      );
      expect(call).toBeDefined();
      expect(parseBody(call?.[1] as RequestInit)).toEqual({
        project_path: 'D:/repo',
        url: 'http://localhost:4173',
        visible: true,
        bounds: { x: 120, y: 80, width: 420, height: 320 },
      });
    });
    expect(window.devicePixelRatio).toBe(2);
  });

  it('expands when the agent opens the shared browser', async () => {
    installBrowserApi(snapshot('attached'));
    render(<AgentBrowserPanel projectId="project-1" projectPath="D:/repo" />);

    expect(screen.queryByTestId('agent-browser-panel')).not.toBeInTheDocument();
    act(() => {
      hostEventListeners.get('agent-browser:show')?.(snapshot('attached'));
    });

    expect(await screen.findByTestId('agent-browser-panel')).toBeInTheDocument();
    expect(screen.getByTestId('agent-browser-debug-status')).toHaveTextContent('AI 可调试');
  });

  it('ignores a delayed show event from the previous project', () => {
    installBrowserApi(snapshot());
    render(<AgentBrowserPanel projectId="project-2" projectPath="D:/repo-2" />);

    act(() => {
      hostEventListeners.get('agent-browser:show')?.(
        snapshot('attached', {
          projectId: 'project-1',
          projectPath: 'D:/repo-1',
        }),
      );
    });

    expect(screen.queryByTestId('agent-browser-panel')).not.toBeInTheDocument();
  });

  it('restores an already active shared browser after remounting', async () => {
    installBrowserApi(snapshot('attached'));
    render(<AgentBrowserPanel projectId="project-1" projectPath="D:/repo" />);

    expect(await screen.findByTestId('agent-browser-panel')).toBeInTheDocument();
    expect(screen.getByTestId('agent-browser-debug-status')).toHaveTextContent('AI 可调试');
  });

  it('updates native view bounds and hides it when collapsed', async () => {
    installBrowserApi(snapshot('attached'));
    render(<AgentBrowserPanel projectPath="D:/repo" />);

    fireEvent.click(screen.getByRole('button', { name: '打开开发浏览器' }));
    expect(await screen.findByText('AI 可调试')).toBeInTheDocument();
    const viewport = screen.getByTestId('agent-browser-viewport');
    vi.spyOn(viewport, 'getBoundingClientRect').mockReturnValue({
      x: 240,
      y: 96,
      left: 240,
      top: 96,
      right: 640,
      bottom: 396,
      width: 400,
      height: 300,
      toJSON: () => ({}),
    });
    resizeCallback?.([], {} as ResizeObserver);

    await waitFor(() => {
      expect(hostApiFetchMock.mock.calls.some(([path, init]) => (
        path === '/api/agent-browser/present'
        && parseBody(init as RequestInit).visible === true
        && JSON.stringify(parseBody(init as RequestInit).bounds)
          === JSON.stringify({ x: 240, y: 96, width: 400, height: 300 })
      ))).toBe(true);
    });

    fireEvent.click(screen.getByRole('button', { name: '收起开发浏览器' }));

    await waitFor(() => {
      expect(hostApiFetchMock.mock.calls.some(([path, init]) => (
        path === '/api/agent-browser/present'
        && parseBody(init as RequestInit).visible === false
      ))).toBe(true);
    });
  });

  it('hides the native view while a renderer modal is open', async () => {
    installBrowserApi(snapshot('attached'));
    render(<AgentBrowserPanel projectPath="D:/repo" />);

    const viewport = await screen.findByTestId('agent-browser-viewport');
    vi.spyOn(viewport, 'getBoundingClientRect').mockReturnValue({
      x: 240,
      y: 96,
      left: 240,
      top: 96,
      right: 640,
      bottom: 396,
      width: 400,
      height: 300,
      toJSON: () => ({}),
    });
    resizeCallback?.([], {} as ResizeObserver);
    await waitFor(() => {
      expect(hostApiFetchMock.mock.calls.some(([path, init]) => (
        path === '/api/agent-browser/present'
        && parseBody(init as RequestInit).visible === true
      ))).toBe(true);
    });
    const beforeModal = hostApiFetchMock.mock.calls.length;
    const dialog = document.createElement('div');
    dialog.setAttribute('role', 'dialog');
    dialog.setAttribute('data-state', 'open');
    act(() => document.body.append(dialog));

    await waitFor(() => {
      expect(hostApiFetchMock.mock.calls.slice(beforeModal).some(([path, init]) => (
        path === '/api/agent-browser/present'
        && parseBody(init as RequestInit).visible === false
      ))).toBe(true);
    });
    const beforeClose = hostApiFetchMock.mock.calls.length;
    act(() => dialog.remove());

    await waitFor(() => {
      expect(hostApiFetchMock.mock.calls.slice(beforeClose).some(([path, init]) => (
        path === '/api/agent-browser/present'
        && parseBody(init as RequestInit).visible === true
      ))).toBe(true);
    });
  });

  it('derives Console and Network panels from the same CDP event page', async () => {
    installBrowserApi(snapshot('attached'), [
      {
        sequence: 1,
        generation: 1,
        timestamp: 1_000,
        method: 'Runtime.consoleAPICalled',
        params: {
          type: 'log',
          args: [{ type: 'string', value: 'ready' }],
        },
      },
      {
        sequence: 2,
        generation: 1,
        timestamp: 1_010,
        method: 'Network.requestWillBeSent',
        params: {
          requestId: 'req-1',
          type: 'Fetch',
          request: { method: 'GET', url: 'http://localhost:4173/api/data' },
        },
      },
      {
        sequence: 3,
        generation: 1,
        timestamp: 1_030,
        method: 'Network.responseReceived',
        params: {
          requestId: 'req-1',
          type: 'Fetch',
          response: {
            url: 'http://localhost:4173/api/data',
            status: 200,
            statusText: 'OK',
            mimeType: 'application/json',
          },
        },
      },
      {
        sequence: 4,
        generation: 1,
        timestamp: 1_060,
        method: 'Network.loadingFinished',
        params: { requestId: 'req-1', encodedDataLength: 42 },
      },
    ]);
    render(<AgentBrowserPanel projectPath="D:/repo" />);

    fireEvent.click(screen.getByRole('button', { name: '打开开发浏览器' }));
    expect(await screen.findByText('ready')).toBeInTheDocument();

    fireEvent.click(screen.getByRole('tab', { name: /Network/ }));
    expect(await screen.findByText('http://localhost:4173/api/data')).toBeInTheDocument();
    expect(screen.getByText('200')).toBeInTheDocument();
    expect(screen.getByText('50ms')).toBeInTheDocument();
  });
});
