/**
 * @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 { MemoryRouter } from 'react-router-dom';
import DashboardPage, { formatShareTime } from './DashboardPage';
import { mockAgent, mockShare } from '../test/fixtures';
import { routerFuture } from '../routerFuture';
import { useWebSocket } from '../hooks/useWebSocket';
import { api } from '../api/client';

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

vi.mock('../components/Visual/3D/FleetTopologyMap', () => ({
  default: () => <div data-testid="fleet-topology-map" />,
}));

vi.mock('../components/Visual/MatrixStreamOverlay', () => ({
  default: () => null,
}));

const useWebSocketMock = vi.mocked(useWebSocket);

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

function renderDashboard() {
  return render(
    <MemoryRouter future={routerFuture}>
      <DashboardPage />
    </MemoryRouter>
  );
}

describe('formatShareTime', () => {
  it('formats ISO timestamps as locale time strings', () => {
    const iso = '2026-05-30T12:00:00.000Z';
    expect(formatShareTime(iso)).toBe(new Date(iso).toLocaleTimeString());
  });
});

describe('DashboardPage', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    localStorage.clear();
    useWebSocketMock.mockReturnValue(wsValue());
    vi.spyOn(api, 'getRecentShares').mockResolvedValue([]);
    vi.spyOn(api, 'listBuilds').mockResolvedValue([]);
    vi.spyOn(api, 'getConfig').mockResolvedValue({
      port: 8080,
      data_dir: '',
      pool: {} as never,
      wallet: {} as never,
      server: { dashboard_subtitle: 'custom subtitle from server' },
      alerts: {} as never,
    });
    vi.spyOn(api, 'getAlerts').mockResolvedValue([]);
    vi.spyOn(api, 'getPoolStatus').mockResolvedValue([]);
    vi.spyOn(api, 'getAIActivity').mockResolvedValue([]);
    vi.spyOn(api, 'getXmrPrice').mockResolvedValue({ usd: 165.5, updated_at: '' });
    vi.spyOn(api, 'getEarningsEstimate').mockResolvedValue({
      xmr_per_day: 0.01,
      usd_per_day: 1.65,
      network_hashrate: 1e9,
    });
    vi.spyOn(api, 'sendBulkCommand').mockResolvedValue({
      success: true,
      sent: 1,
      failed: 0,
      action: 'restart',
    });
  });

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

  it('renders hero heading and key section titles', () => {
    renderDashboard();
    expect(screen.getByRole('heading', { level: 1, name: 'Command Deck' })).toBeInTheDocument();
    expect(screen.getByText('Fleet Pipeline')).toBeInTheDocument();
    expect(screen.getByText('Share Activity Pulse')).toBeInTheDocument();
    expect(screen.getByText('Machine Roster')).toBeInTheDocument();
    expect(screen.getByText('PERSONAL NETWORK · LIVE TELEMETRY')).toBeInTheDocument();
  });

  it('shows reconnecting label when websocket is down', () => {
    useWebSocketMock.mockReturnValue(wsValue({ isConnected: false }));
    renderDashboard();
    expect(screen.getByText('RECONNECTING')).toBeInTheDocument();
  });

  it('shows signal locked when websocket is connected', () => {
    renderDashboard();
    expect(screen.getByText('SIGNAL LOCKED')).toBeInTheDocument();
    expect(screen.getByText('0 nodes registered')).toBeInTheDocument();
  });

  it('loads dashboard subtitle from config API', async () => {
    renderDashboard();
    expect(await screen.findByText('custom subtitle from server')).toBeInTheDocument();
    expect(api.getConfig).toHaveBeenCalled();
  });

  it('shows empty roster state when no agents', async () => {
    renderDashboard();
    expect(await screen.findByText('No miners on the wire')).toBeInTheDocument();
  });

  it('does not show projection or fake earnings with no agents', async () => {
    renderDashboard();
    expect(await screen.findByText('Command Deck')).toBeInTheDocument();
    expect(screen.queryByText(/Projection mode/i)).not.toBeInTheDocument();
    expect(screen.queryByText('Target Fleet Earnings')).not.toBeInTheDocument();
    expect(screen.queryByText('Vault-01')).not.toBeInTheDocument();
  });

  it('renders stat labels and top agent card', async () => {
    const agent = mockAgent({ name: 'Alpha Node', hashrate_15m: 1200 });
    useWebSocketMock.mockReturnValue(wsValue({ agents: [agent] }));
    renderDashboard();
    expect(await screen.findByText('Top Miner')).toBeInTheDocument();
    expect(screen.getByText('Fleet Compute')).toBeInTheDocument();
    expect(screen.getAllByText('Fleet Hash')[0]).toBeInTheDocument();
    const roster = screen.getByText('Machine Roster').closest('section') as HTMLElement;
    expect(within(roster).getByText('Alpha Node')).toBeInTheDocument();
    expect(within(roster).getByText('online')).toBeInTheDocument();
  });

  it('toggles advanced mode and reveals share log section', async () => {
    const user = userEvent.setup();
    vi.spyOn(api, 'getRecentShares').mockResolvedValue([mockShare()]);
    renderDashboard();
    expect(screen.queryByText('Share Log')).not.toBeInTheDocument();
    await user.click(screen.getByRole('button', { name: '[ADVANCED]' }));
    expect(screen.getByText('Share Log')).toBeInTheDocument();
    expect(localStorage.getItem('aether-dash-advanced')).toBe('1');
  });

  it('shows share log rows in advanced mode with stable keys', async () => {
    const share = mockShare({ id: undefined as unknown as number, hash: 'deadbeef' });
    vi.spyOn(api, 'getRecentShares').mockResolvedValue([share]);
    localStorage.setItem('aether-dash-advanced', '1');
    useWebSocketMock.mockReturnValue(wsValue({ agents: [mockAgent()] }));
    renderDashboard();
    expect(await screen.findByText('Share Log')).toBeInTheDocument();
    expect(screen.getByText('Accepted')).toBeInTheDocument();
  });

  it('alerts when bulk command API fails', async () => {
    const agent = mockAgent();
    useWebSocketMock.mockReturnValue(wsValue({ agents: [agent] }));
    vi.spyOn(api, 'sendBulkCommand').mockRejectedValue(new Error('network down'));
    const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {});
    renderDashboard();
    const roster = await screen.findByText('Machine Roster');
    const section = roster.closest('section') as HTMLElement;
    await waitFor(() => expect(within(section).getByText(agent.name)).toBeInTheDocument());
    const user = userEvent.setup();
    const grid = section.querySelector('.agent-grid') as HTMLElement;
    await user.click(within(grid).getByRole('checkbox'));
    const bulkBar = section.querySelector('.fleet-bulk-bar') as HTMLElement;
    await user.click(within(bulkBar).getByRole('button', { name: 'Pause' }));
    await waitFor(() => {
      expect(alertSpy).toHaveBeenCalledWith('network down');
    });
    alertSpy.mockRestore();
  });

  it('prefers live websocket alerts over REST fallback', async () => {
    useWebSocketMock.mockReturnValue(
      wsValue({
        fleetAlerts: [{ id: '1', level: 'warn', type: 'pool_down', message: 'live alert', timestamp: '' }],
      })
    );
    vi.spyOn(api, 'getAlerts').mockResolvedValue([
      { id: '2', level: 'warn', type: 'stale', message: 'rest alert', timestamp: '' },
    ]);
    renderDashboard();
    expect(await screen.findByText('live alert')).toBeInTheDocument();
    expect(screen.queryByText('rest alert')).not.toBeInTheDocument();
  });
});
