import { describe, it, expect, vi, beforeEach } from 'vitest';
import { EventEmitter } from 'events';

vi.mock('electron', () => ({
  ipcMain: { handle: vi.fn(), on: vi.fn() },
  BrowserWindow: {
    getAllWindows: vi.fn(() => []),
  },
}));

import { ipcMain, BrowserWindow } from 'electron';
import type { IpcMainInvokeEvent } from 'electron';
import { setupA2AIPC } from './a2a';
import type { A2ARelayModeService, AgentCardRegistry, CredentialStore, TaskArtifactUpdateEvent, TaskManager, TaskStatusUpdateEvent } from '@chamber/services';

// Helper to keep test ergonomics — the IPC handler signature demands
// IpcMainInvokeEvent / BrowserWindow instances we don't need in unit tests.
const EVT = {} as IpcMainInvokeEvent;
const asWindows = (wins: { webContents: { send: unknown } }[]): Electron.BrowserWindow[] =>
  wins as unknown as Electron.BrowserWindow[];
type InvokeHandler = (event: IpcMainInvokeEvent, ...args: unknown[]) => unknown;
const getHandler = (name: string): InvokeHandler => {
  const call = vi.mocked(ipcMain.handle).mock.calls.find((c) => c[0] === name);
  if (!call) throw new Error(`no handler registered for ${name}`);
  return call[1] as InvokeHandler;
};

const mockRegistry = {
  getCards: vi.fn(() => [
    { mindId: 'agent-a', name: 'Agent A' },
    { mindId: 'agent-b', name: 'Agent B' },
  ]),
};

const mockTaskManager = {
  getTask: vi.fn(),
  listTasks: vi.fn(),
  cancelTask: vi.fn(),
};

const makeConfigStore = (relayBaseUrl?: string, authMode?: 'static' | 'interactive') => {
  const config = {
    version: 2 as const,
    minds: [],
    activeMindId: null,
    activeLogin: null,
    theme: 'dark' as const,
    ...(relayBaseUrl ? { a2aRelayBaseUrl: relayBaseUrl } : {}),
    ...(authMode ? { a2aRelayAuthMode: authMode } : {}),
  };
  return {
    config,
    load: vi.fn(() => config),
    save: vi.fn((next) => {
      Object.assign(config, next);
    }),
  };
};

const makeCredentialStore = (entries: { account: string; password: string }[] = []) => ({
  findCredentials: vi.fn(async () => entries),
  setPassword: vi.fn(async (service: string, account: string, password: string) => {
    const existing = entries.find((entry) => entry.account === account);
    if (existing) {
      existing.password = password;
    } else {
      entries.push({ account, password });
    }
    void service;
  }),
  deletePassword: vi.fn(async () => true),
});

describe('A2A IPC', () => {
  let ipcEmitter: EventEmitter;

  beforeEach(() => {
    vi.clearAllMocks();
    ipcEmitter = new EventEmitter();
    setupA2AIPC(ipcEmitter, mockRegistry as unknown as AgentCardRegistry, mockTaskManager as unknown as TaskManager);
  });

  it('a2a:incoming forwards to all windows', () => {
    const mockWebContents1 = { send: vi.fn() };
    const mockWebContents2 = { send: vi.fn() };
    vi.mocked(BrowserWindow.getAllWindows).mockReturnValue(asWindows([
      { webContents: mockWebContents1 },
      { webContents: mockWebContents2 },
    ]));

    const payload = {
      targetMindId: 'agent-b',
      message: { messageId: 'msg-1', role: 'ROLE_USER', parts: [{ text: 'Hello' }] },
      replyMessageId: 'reply-1',
    };
    ipcEmitter.emit('a2a:incoming', payload);

    expect(mockWebContents1.send).toHaveBeenCalledWith('a2a:incoming', payload);
    expect(mockWebContents2.send).toHaveBeenCalledWith('a2a:incoming', payload);
  });

  it('a2a:incoming payload includes message and replyMessageId', () => {
    const mockWebContents = { send: vi.fn() };
    vi.mocked(BrowserWindow.getAllWindows).mockReturnValue(asWindows([{ webContents: mockWebContents }]));

    const payload = {
      targetMindId: 'agent-b',
      message: { messageId: 'msg-1', role: 'ROLE_USER', parts: [{ text: 'Test' }], metadata: { fromId: 'agent-a', fromName: 'Agent A' } },
      replyMessageId: 'reply-msg-1',
    };
    ipcEmitter.emit('a2a:incoming', payload);

    const sent = mockWebContents.send.mock.calls[0];
    expect(sent[0]).toBe('a2a:incoming');
    expect(sent[1]).toHaveProperty('message');
    expect(sent[1]).toHaveProperty('replyMessageId', 'reply-msg-1');
    expect(sent[1]).toHaveProperty('targetMindId', 'agent-b');
  });

  it('e2e:a2a:incoming emits incoming payload only when E2E mode is enabled', async () => {
    const previous = process.env.CHAMBER_E2E;
    process.env.CHAMBER_E2E = '1';
    vi.clearAllMocks();
    ipcEmitter = new EventEmitter();
    setupA2AIPC(ipcEmitter, mockRegistry as unknown as AgentCardRegistry, mockTaskManager as unknown as TaskManager);

    const payload = {
      targetMindId: 'agent-b',
      message: { messageId: 'msg-1', role: 'ROLE_USER' as const, parts: [{ text: 'Test' }], metadata: { fromId: 'agent-a', fromName: 'Agent A' } },
      replyMessageId: 'reply-msg-1',
    };
    const incoming = new Promise((resolve) => {
      ipcEmitter.once('a2a:incoming', resolve);
    });

    try {
      await getHandler('e2e:a2a:incoming')(EVT, payload);
      await expect(incoming).resolves.toEqual(payload);
    } finally {
      if (previous === undefined) {
        delete process.env.CHAMBER_E2E;
      } else {
        process.env.CHAMBER_E2E = previous;
      }
    }
  });

  it('a2a:listAgents returns cards from registry', async () => {
    const result = await getHandler('a2a:listAgents')(EVT);
    expect(result).toEqual([
      { mindId: 'agent-a', name: 'Agent A' },
      { mindId: 'agent-b', name: 'Agent B' },
    ]);
    expect(mockRegistry.getCards).toHaveBeenCalled();
  });

  // --- Task IPC tests ---

  it('task:status-update event forwarded to all BrowserWindows', () => {
    const wc1 = { send: vi.fn() };
    const wc2 = { send: vi.fn() };
    vi.mocked(BrowserWindow.getAllWindows).mockReturnValue(asWindows([
      { webContents: wc1 },
      { webContents: wc2 },
    ]));

    const payload: TaskStatusUpdateEvent = {
      taskId: 'task-1',
      contextId: 'ctx-1',
      status: { state: 'TASK_STATE_WORKING' },
    };
    ipcEmitter.emit('task:status-update', payload);

    expect(wc1.send).toHaveBeenCalledWith('a2a:task-status-update', payload);
    expect(wc2.send).toHaveBeenCalledWith('a2a:task-status-update', payload);
  });

  it('task:artifact-update event forwarded to all BrowserWindows', () => {
    const wc1 = { send: vi.fn() };
    const wc2 = { send: vi.fn() };
    vi.mocked(BrowserWindow.getAllWindows).mockReturnValue(asWindows([
      { webContents: wc1 },
      { webContents: wc2 },
    ]));

    const payload: TaskArtifactUpdateEvent = {
      taskId: 'task-1',
      contextId: 'ctx-1',
      artifact: { artifactId: 'art-1', parts: [{ text: 'result' }] },
      lastChunk: true,
    };
    ipcEmitter.emit('task:artifact-update', payload);

    expect(wc1.send).toHaveBeenCalledWith('a2a:task-artifact-update', payload);
    expect(wc2.send).toHaveBeenCalledWith('a2a:task-artifact-update', payload);
  });

  it('a2a:getTask handle returns task from TaskManager', async () => {
    const task = { id: 'task-1', contextId: 'ctx-1', status: { state: 'TASK_STATE_COMPLETED' } };
    mockTaskManager.getTask.mockReturnValue(task);

    const result = await getHandler('a2a:getTask')(EVT, 'task-1', 5);
    expect(mockTaskManager.getTask).toHaveBeenCalledWith('task-1', 5);
    expect(result).toEqual(task);
  });

  it('a2a:listTasks handle returns task list from TaskManager', async () => {
    const response = { tasks: [], nextPageToken: '', pageSize: 0, totalSize: 0 };
    mockTaskManager.listTasks.mockReturnValue(response);

    const filter = { contextId: 'ctx-1', status: 'TASK_STATE_WORKING' };
    const result = await getHandler('a2a:listTasks')(EVT, filter);
    expect(mockTaskManager.listTasks).toHaveBeenCalledWith(filter);
    expect(result).toEqual(response);
  });

  it('a2a:listTasks drops invalid status values at the IPC boundary', async () => {
    const response = { tasks: [], nextPageToken: '', pageSize: 0, totalSize: 0 };
    mockTaskManager.listTasks.mockReturnValue(response);

    await getHandler('a2a:listTasks')(EVT, { contextId: 'ctx-1', status: 'bogus-status' });
    expect(mockTaskManager.listTasks).toHaveBeenCalledWith({ contextId: 'ctx-1', status: undefined });
  });

  it('a2a:cancelTask handle returns updated task', async () => {
    const task = { id: 'task-1', contextId: 'ctx-1', status: { state: 'TASK_STATE_CANCELED' } };
    mockTaskManager.cancelTask.mockReturnValue(task);

    const result = await getHandler('a2a:cancelTask')(EVT, 'task-1');
    expect(mockTaskManager.cancelTask).toHaveBeenCalledWith('task-1');
    expect(result).toEqual(task);
  });

  it('a2a:cancelTask rejects when TaskManager throws', async () => {
    mockTaskManager.cancelTask.mockImplementation(() => {
      throw new Error('Task task-1 not found');
    });

    await expect(getHandler('a2a:cancelTask')(EVT, 'task-1')).rejects.toThrow('Task task-1 not found');
  });

  it('a2a:relayConnect forwards interactive auth options without requiring a static token', async () => {
    const relayModeService = makeRelayModeService();
    vi.clearAllMocks();
    setupA2AIPC(
      ipcEmitter,
      mockRegistry as unknown as AgentCardRegistry,
      mockTaskManager as unknown as TaskManager,
      { relayModeService: relayModeService as unknown as A2ARelayModeService },
    );

    await getHandler('a2a:relay-connect')(EVT, {
      relayBaseUrl: 'https://switchboard.example.com',
      authMode: 'interactive',
      clientId: 'client-id',
      tenantId: 'common',
      scope: 'api://client-id/user_impersonation',
    });

    expect(relayModeService.connect).toHaveBeenCalledWith(expect.objectContaining({
      baseUrl: 'https://switchboard.example.com',
      authProvider: expect.objectContaining({ getAuthorizationHeader: expect.any(Function) }),
    }));
    expect(JSON.stringify(relayModeService.connect.mock.calls[0][0])).not.toContain('accessToken');
    expect(JSON.stringify(relayModeService.connect.mock.calls[0][0])).not.toContain('refreshToken');
  });

  it('a2a:relayConnect wires interactive auth to the stored Switchboard token cache', async () => {
    const relayModeService = makeRelayModeService();
    const previousFetch = globalThis.fetch;
    const fetchMock = vi.fn<typeof fetch>(async (_input, init) => {
      const body = new URLSearchParams(String(init?.body));
      expect(body.get('grant_type')).toBe('refresh_token');
      expect(body.get('refresh_token')).toBe('cached-refresh-token');
      return new Response(JSON.stringify({
        access_token: 'refreshed-access-token',
        refresh_token: 'next-refresh-token',
        expires_in: 3600,
      }), {
        status: 200,
        headers: { 'content-type': 'application/json' },
      });
    });
    globalThis.fetch = fetchMock;
    const credentialStore = makeCredentialStore([
      {
        account: 'https://switchboard.example.com|client-id|common|api://client-id/user_impersonation',
        password: JSON.stringify({ refreshToken: 'cached-refresh-token' }),
      },
    ]);
    try {
      vi.clearAllMocks();
      setupA2AIPC(
        ipcEmitter,
        mockRegistry as unknown as AgentCardRegistry,
        mockTaskManager as unknown as TaskManager,
        {
          relayModeService: relayModeService as unknown as A2ARelayModeService,
          credentialStore: credentialStore as unknown as CredentialStore,
        },
      );

      await getHandler('a2a:relay-connect')(EVT, {
        relayBaseUrl: 'https://switchboard.example.com',
        authMode: 'interactive',
        clientId: 'client-id',
        tenantId: 'common',
        scope: 'api://client-id/user_impersonation',
      });

      const options = relayModeService.connect.mock.calls[0][0] as { authProvider: { getAuthorizationHeader: () => Promise<string> } };
      await expect(options.authProvider.getAuthorizationHeader()).resolves.toBe('Bearer refreshed-access-token');
      expect(credentialStore.setPassword).toHaveBeenCalledWith(
        'chamber-a2a-relay-entra',
        'https://switchboard.example.com|client-id|common|api://client-id/user_impersonation',
        JSON.stringify({ refreshToken: 'next-refresh-token' }),
      );
    } finally {
      globalThis.fetch = previousFetch;
    }
  });

  it('a2a:relayConnect can use built-in Entra defaults for interactive auth', async () => {
    const relayModeService = makeRelayModeService();
    vi.clearAllMocks();
    setupA2AIPC(
      ipcEmitter,
      mockRegistry as unknown as AgentCardRegistry,
      mockTaskManager as unknown as TaskManager,
      { relayModeService: relayModeService as unknown as A2ARelayModeService },
    );

    await getHandler('a2a:relay-connect')(EVT, {
      relayBaseUrl: 'https://switchboard.example.com',
      authMode: 'interactive',
    });

    expect(relayModeService.connect).toHaveBeenCalledWith(expect.objectContaining({
      baseUrl: 'https://switchboard.example.com',
      authProvider: expect.objectContaining({ getAuthorizationHeader: expect.any(Function) }),
    }));
  });

  it('a2a:relayStatus returns the saved relay URL and stored-token availability when disconnected', async () => {
    const configStore = makeConfigStore('https://switchboard.example.com', 'static');
    const credentialStore = makeCredentialStore([
      { account: 'https://switchboard.example.com', password: 'stored-token' },
    ]);
    vi.clearAllMocks();
    setupA2AIPC(
      ipcEmitter,
      mockRegistry as unknown as AgentCardRegistry,
      mockTaskManager as unknown as TaskManager,
      { configStore, credentialStore: credentialStore as unknown as CredentialStore },
    );

    await expect(getHandler('a2a:relay-status')(EVT)).resolves.toEqual(expect.objectContaining({
      state: 'disconnected',
      relayBaseUrl: 'https://switchboard.example.com',
      authMode: 'static',
      hasStoredRelayToken: true,
    }));
  });

  it('a2a:relayConnect saves the relay URL and auth mode after connecting', async () => {
    const relayModeService = makeRelayModeService();
    const configStore = makeConfigStore();
    vi.clearAllMocks();
    setupA2AIPC(
      ipcEmitter,
      mockRegistry as unknown as AgentCardRegistry,
      mockTaskManager as unknown as TaskManager,
      { relayModeService: relayModeService as unknown as A2ARelayModeService, configStore },
    );

    await getHandler('a2a:relay-connect')(EVT, {
      relayBaseUrl: 'https://switchboard.example.com',
      authMode: 'interactive',
    });

    expect(configStore.save).toHaveBeenCalledWith(expect.objectContaining({
      a2aRelayBaseUrl: 'https://switchboard.example.com',
      a2aRelayAuthMode: 'interactive',
    }));
  });

  it('a2a:relayConnect in auto mode checks the stored static token before interactive auth', async () => {
    const relayModeService = makeRelayModeService();
    const credentialStore = makeCredentialStore([
      { account: 'https://switchboard.example.com', password: 'stored-token' },
    ]);
    vi.clearAllMocks();
    setupA2AIPC(
      ipcEmitter,
      mockRegistry as unknown as AgentCardRegistry,
      mockTaskManager as unknown as TaskManager,
      {
        relayModeService: relayModeService as unknown as A2ARelayModeService,
        credentialStore: credentialStore as unknown as CredentialStore,
      },
    );

    await getHandler('a2a:relay-connect')(EVT, {
      relayBaseUrl: 'https://switchboard.example.com',
      authMode: 'auto',
    });

    const options = relayModeService.connect.mock.calls[0][0] as { authProvider: { getAuthorizationHeader: () => Promise<string> } };
    await expect(options.authProvider.getAuthorizationHeader()).resolves.toBe('Bearer stored-token');
  });

  it('a2a:relayConnect in auto mode saves interactive auth when no static token exists', async () => {
    const relayModeService = makeRelayModeService();
    const configStore = makeConfigStore();
    vi.clearAllMocks();
    setupA2AIPC(
      ipcEmitter,
      mockRegistry as unknown as AgentCardRegistry,
      mockTaskManager as unknown as TaskManager,
      {
        relayModeService: relayModeService as unknown as A2ARelayModeService,
        configStore,
      },
    );

    await getHandler('a2a:relay-connect')(EVT, {
      relayBaseUrl: 'https://switchboard.example.com',
      authMode: 'auto',
    });

    expect(configStore.save).toHaveBeenCalledWith(expect.objectContaining({
      a2aRelayBaseUrl: 'https://switchboard.example.com',
      a2aRelayAuthMode: 'interactive',
    }));
  });

  it('a2a:relayConnect forwards static auth options through a static auth provider', async () => {
    const relayModeService = makeRelayModeService();
    vi.clearAllMocks();
    setupA2AIPC(
      ipcEmitter,
      mockRegistry as unknown as AgentCardRegistry,
      mockTaskManager as unknown as TaskManager,
      { relayModeService: relayModeService as unknown as A2ARelayModeService },
    );

    await getHandler('a2a:relay-connect')(EVT, {
      relayBaseUrl: 'http://127.0.0.1:4317',
      authMode: 'static',
      relayToken: 'relay-token',
    });

    const options = relayModeService.connect.mock.calls[0][0] as { authProvider: { getAuthorizationHeader: () => Promise<string> } };
    await expect(options.authProvider.getAuthorizationHeader()).resolves.toBe('Bearer relay-token');
  });

  it('a2a:relayConnect saves static relay tokens in the credential store after connecting', async () => {
    const relayModeService = makeRelayModeService();
    const credentialStore = makeCredentialStore();
    vi.clearAllMocks();
    setupA2AIPC(
      ipcEmitter,
      mockRegistry as unknown as AgentCardRegistry,
      mockTaskManager as unknown as TaskManager,
      {
        relayModeService: relayModeService as unknown as A2ARelayModeService,
        credentialStore: credentialStore as unknown as CredentialStore,
      },
    );

    await getHandler('a2a:relay-connect')(EVT, {
      relayBaseUrl: 'http://127.0.0.1:4317',
      authMode: 'static',
      relayToken: 'relay-token',
    });

    expect(credentialStore.setPassword).toHaveBeenCalledWith(
      'chamber-a2a-relay',
      'http://127.0.0.1:4317',
      'relay-token',
    );
  });

  it('a2a:relayConnect reuses a stored static relay token when the request omits it', async () => {
    const relayModeService = makeRelayModeService();
    const credentialStore = makeCredentialStore([
      { account: 'http://127.0.0.1:4317', password: 'stored-token' },
    ]);
    vi.clearAllMocks();
    setupA2AIPC(
      ipcEmitter,
      mockRegistry as unknown as AgentCardRegistry,
      mockTaskManager as unknown as TaskManager,
      {
        relayModeService: relayModeService as unknown as A2ARelayModeService,
        credentialStore: credentialStore as unknown as CredentialStore,
      },
    );

    await getHandler('a2a:relay-connect')(EVT, {
      relayBaseUrl: 'http://127.0.0.1:4317',
      authMode: 'static',
    });

    const options = relayModeService.connect.mock.calls[0][0] as { authProvider: { getAuthorizationHeader: () => Promise<string> } };
    await expect(options.authProvider.getAuthorizationHeader()).resolves.toBe('Bearer stored-token');
    expect(credentialStore.setPassword).not.toHaveBeenCalled();
  });

  it('a2a:relayConnect does not save static relay tokens when connect fails', async () => {
    const relayModeService = makeRelayModeService();
    relayModeService.connect.mockRejectedValueOnce(new Error('connect failed'));
    const credentialStore = makeCredentialStore();
    vi.clearAllMocks();
    setupA2AIPC(
      ipcEmitter,
      mockRegistry as unknown as AgentCardRegistry,
      mockTaskManager as unknown as TaskManager,
      {
        relayModeService: relayModeService as unknown as A2ARelayModeService,
        credentialStore: credentialStore as unknown as CredentialStore,
      },
    );

    await expect(getHandler('a2a:relay-connect')(EVT, {
      relayBaseUrl: 'http://127.0.0.1:4317',
      authMode: 'static',
      relayToken: 'relay-token',
    })).rejects.toThrow('connect failed');

    expect(credentialStore.setPassword).not.toHaveBeenCalled();
  });

  it('a2a:relayConnect rejects static auth without a request token or stored token', async () => {
    const relayModeService = makeRelayModeService();
    vi.clearAllMocks();
    setupA2AIPC(
      ipcEmitter,
      mockRegistry as unknown as AgentCardRegistry,
      mockTaskManager as unknown as TaskManager,
      { relayModeService: relayModeService as unknown as A2ARelayModeService },
    );

    await expect(getHandler('a2a:relay-connect')(EVT, {
      relayBaseUrl: 'http://127.0.0.1:4317',
      authMode: 'static',
    })).rejects.toThrow('A2A relay token is not configured');
    expect(relayModeService.connect).not.toHaveBeenCalled();
  });
});

function makeRelayModeService() {
  return {
    connect: vi.fn(async (options: unknown) => {
      void options;
      return undefined;
    }),
    disconnect: vi.fn(async () => undefined),
    isConnected: vi.fn(() => true),
    getPublishedAgentCount: vi.fn(() => 1),
    getRelayAgentCount: vi.fn(async () => 2),
    getLastPollError: vi.fn(() => null),
  };
}
