/**
 * @vitest-environment happy-dom
 */
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { cleanup, render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import AgentsPage from './AgentsPage';
import { mockAgent, mockServerInfo } from '../test/fixtures';
import { useWebSocket } from '../hooks/useWebSocket';
import { api } from '../api/client';

vi.mock('../hooks/useWebSocket', () => ({
  useWebSocket: vi.fn(),
}));

vi.mock('../components/Fleet/AgentRemoteActions', () => ({
  default: () => <div data-testid="agent-remote-actions-mock" />,
}));

const useWebSocketMock = vi.mocked(useWebSocket);

function wsValue(overrides: Partial<ReturnType<typeof useWebSocket>> = {}) {
  return {
    isConnected: false,
    agents: [],
    recentShares: [],
    fleetAlerts: [],
    poolStatus: [],
    aiActivity: [],
    agentLogs: {},
    commandResults: [],
    latestMessage: null,
    ...overrides,
  };
}

function renderAgentsPage() {
  return render(<AgentsPage />);
}

describe('AgentsPage', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    useWebSocketMock.mockReturnValue(wsValue());
    vi.spyOn(api, 'listAgents').mockResolvedValue([]);
    vi.spyOn(api, 'getServerInfo').mockResolvedValue(mockServerInfo);
    vi.spyOn(api, 'getAgentStats').mockResolvedValue([]);
    vi.spyOn(api, 'getAgentLog').mockResolvedValue({ agent_id: 'x', content: 'log line' });
    vi.spyOn(api, 'updateAgentMeta').mockResolvedValue({
      success: true,
      agent: mockAgent({ notes: 'saved note', tags: ['rack-a'] }),
    });
    vi.spyOn(api, 'sendBulkCommand').mockResolvedValue({
      success: true,
      sent: 1,
      failed: 0,
      action: 'restart',
    });
  });

  afterEach(() => {
    cleanup();
  });

  it('renders page heading and quick deploy labels', async () => {
    renderAgentsPage();
    expect(screen.getByRole('heading', { level: 1, name: 'Fleet Roster' })).toBeInTheDocument();
    expect(screen.getByText('FLEET REGISTRY')).toBeInTheDocument();
    await waitFor(() => {
      expect(screen.getByText('One-liner Quick Deploy')).toBeInTheDocument();
    });
    expect(screen.getByText('Install & run (auto-launches)')).toBeInTheDocument();
    expect(screen.getByText('Direct download only (saves file)')).toBeInTheDocument();
    expect(screen.getAllByText('Windows')).toHaveLength(2);
    expect(screen.getByText('Linux/Mac')).toBeInTheDocument();
    expect(screen.getByText('macOS')).toBeInTheDocument();
  });

  it('builds quick deploy URLs from server info', async () => {
    renderAgentsPage();
    await waitFor(() => {
      expect(screen.getAllByText(`iex (irm '${mockServerInfo.suggested_url}/install.ps1')`)).toHaveLength(1);
    });
    expect(screen.getByText(`curl -sL ${mockServerInfo.suggested_url}/install.sh | bash`)).toBeInTheDocument();
    expect(screen.getByText(`${mockServerInfo.suggested_url}/get?os=windows`)).toBeInTheDocument();
  });

  it('shows loading then empty multi-OS state', async () => {
    renderAgentsPage();
    expect(screen.getByText('Scanning network...')).toBeInTheDocument();
    expect(await screen.findByText('No agents registered')).toBeInTheDocument();
    expect(
      screen.getByText(/Deploy a worker to any machine \(Windows, Linux, or macOS\)/)
    ).toBeInTheDocument();
  });

  it('surfaces listAgents load errors', async () => {
    vi.spyOn(api, 'listAgents').mockRejectedValue(new Error('API unavailable'));
    renderAgentsPage();
    expect(await screen.findByText('API unavailable')).toBeInTheDocument();
  });

  it('lists agents and opens detail panel with section headings', async () => {
    const agent = mockAgent({ name: 'Rack B Miner', notes: 'basement', tags: ['home'] });
    vi.spyOn(api, 'listAgents').mockResolvedValue([agent]);
    renderAgentsPage();
    await waitFor(() => {
      expect(screen.getByText('Rack B Miner')).toBeInTheDocument();
    });
    await userEvent.setup().click(screen.getByText('Rack B Miner'));
    await waitFor(() => {
      expect(screen.getByRole('heading', { level: 2, name: 'Rack B Miner' })).toBeInTheDocument();
    });
    expect(screen.getByText('Notes & Tags')).toBeInTheDocument();
    expect(screen.getByText('Hashrate')).toBeInTheDocument();
    expect(screen.getByText('Shares')).toBeInTheDocument();
    expect(screen.getByText('Remote Control')).toBeInTheDocument();
    expect(api.getAgentStats).toHaveBeenCalledWith(agent.id, 60);
  });

  it('preserves notes draft while typing until agent switch', async () => {
    const a1 = mockAgent({ id: 'a1', name: 'Node One', notes: 'note one' });
    const a2 = mockAgent({ id: 'a2', name: 'Node Two', notes: 'note two' });
    vi.spyOn(api, 'listAgents').mockResolvedValue([a1, a2]);
    renderAgentsPage();
    await waitFor(() => expect(screen.getByText('Node One')).toBeInTheDocument());
    const user = userEvent.setup();
    await user.click(screen.getByText('Node One'));
    const detail = await screen.findByRole('heading', { level: 2, name: 'Node One' });
    const panel = detail.closest('.agent-detail') as HTMLElement;
    const notes = within(panel).getByPlaceholderText('Notes about this machine…') as HTMLTextAreaElement;
    await waitFor(() => expect(notes.value).toBe('note one'));
    await user.clear(notes);
    await user.type(notes, 'typing in progress');
    expect(notes.value).toBe('typing in progress');
    await user.click(screen.getByText('Node Two'));
    await waitFor(() => expect(notes.value).toBe('note two'));
  });

  it('saves notes and tags via API', async () => {
    const agent = mockAgent({ id: 'save-me', name: 'Save Target' });
    vi.spyOn(api, 'listAgents').mockResolvedValue([agent]);
    const updateSpy = vi.spyOn(api, 'updateAgentMeta').mockResolvedValue({
      success: true,
      agent: { ...agent, notes: 'Living room PC', tags: ['living-room'] },
    });
    renderAgentsPage();
    await waitFor(() => expect(screen.getByText('Save Target')).toBeInTheDocument());
    const user = userEvent.setup();
    await user.click(screen.getByText('Save Target'));
    const detail = await screen.findByRole('heading', { level: 2, name: 'Save Target' });
    const panel = detail.closest('.agent-detail') as HTMLElement;
    const notes = within(panel).getByPlaceholderText('Notes about this machine…');
    await user.clear(notes);
    await user.type(notes, 'Living room PC');
    const tags = within(panel).getByPlaceholderText('Tags: living-room, rack-b (comma separated)');
    await user.clear(tags);
    await user.type(tags, 'living-room, rack-b');
    await user.click(within(panel).getByRole('button', { name: 'Save notes & tags' }));
    await waitFor(() => {
      expect(updateSpy).toHaveBeenCalledWith('save-me', 'Living room PC', ['living-room', 'rack-b']);
    });
    expect(await within(panel).findByText('Saved')).toBeInTheDocument();
  });

  it('alerts when bulk action has no online agents', async () => {
    const offline = mockAgent({ id: 'off-1', name: 'Offline Node', status: 'offline' });
    vi.spyOn(api, 'listAgents').mockResolvedValue([offline]);
    const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {});
    renderAgentsPage();
    await waitFor(() => expect(screen.getByText('Offline Node')).toBeInTheDocument());
    const list = screen.getByText('Offline Node').closest('.agents-list') as HTMLElement;
    await userEvent.setup().click(within(list).getByRole('checkbox'));
    await userEvent.setup().click(screen.getByRole('button', { name: 'Pause' }));
    await waitFor(() => {
      expect(alertSpy).toHaveBeenCalledWith('No online agents in selection.');
    });
    alertSpy.mockRestore();
  });

  it('alerts when bulk command API fails', async () => {
    const agent = mockAgent({ name: 'Online One' });
    vi.spyOn(api, 'listAgents').mockResolvedValue([agent]);
    vi.spyOn(api, 'sendBulkCommand').mockRejectedValue(new Error('bulk failed'));
    const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {});
    renderAgentsPage();
    await waitFor(() => expect(screen.getByText('Online One')).toBeInTheDocument());
    const list = screen.getByText('Online One').closest('.agents-list') as HTMLElement;
    await userEvent.setup().click(within(list).getByRole('checkbox'));
    await userEvent.setup().click(screen.getByRole('button', { name: 'Pause' }));
    await waitFor(() => {
      expect(alertSpy).toHaveBeenCalledWith('bulk failed');
    });
    alertSpy.mockRestore();
  });

  it('syncs agents from websocket when connected', async () => {
    const restAgent = mockAgent({ id: 'rest', name: 'REST Name', hashrate_15m: 100 });
    vi.spyOn(api, 'listAgents').mockImplementation(
      () => new Promise((resolve) => setTimeout(() => resolve([restAgent]), 50))
    );
    const liveAgent = mockAgent({ id: 'rest', name: 'Live Name', hashrate_15m: 999 });
    useWebSocketMock.mockReturnValue(wsValue({ isConnected: true, agents: [liveAgent] }));
    renderAgentsPage();
    await waitFor(() => {
      expect(screen.getByText('Live Name')).toBeInTheDocument();
    });
    expect(screen.queryByText('REST Name')).not.toBeInTheDocument();
  });

  it('shows filter empty hint when no agents match', async () => {
    vi.spyOn(api, 'listAgents').mockResolvedValue([mockAgent({ name: 'Hidden', tags: ['prod'] })]);
    renderAgentsPage();
    await waitFor(() => expect(screen.getByText('Hidden')).toBeInTheDocument());
    const search = screen.getByPlaceholderText('Search name, IP, notes, tags…');
    await userEvent.setup().type(search, 'nomatchxyz');
    expect(screen.getByText('No agents match filters.')).toBeInTheDocument();
  });

  it('select all filtered selects every visible agent', async () => {
    const a1 = mockAgent({ id: 'a1', name: 'Alpha', tags: ['prod'] });
    const a2 = mockAgent({ id: 'a2', name: 'Beta', tags: ['prod'] });
    const a3 = mockAgent({ id: 'a3', name: 'Gamma', tags: ['dev'] });
    vi.spyOn(api, 'listAgents').mockResolvedValue([a1, a2, a3]);
    renderAgentsPage();
    await waitFor(() => expect(screen.getByText('Alpha')).toBeInTheDocument());
    const user = userEvent.setup();
    await user.selectOptions(screen.getByTitle('Filter by tag'), 'prod');
    await user.click(screen.getByRole('button', { name: 'Select all filtered (2)' }));
    expect(screen.getByText('2 selected')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: 'Pause' })).toBeInTheDocument();
  });
});
