/**
 * @vitest-environment happy-dom
 */
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { cleanup, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter } from 'react-router-dom';
import { mockAgent } from '../test/fixtures';
import { useWebSocket } from '../hooks/useWebSocket';
import { api } from '../api/client';
import CruciblePage, {
  agentColor,
  patchLabel,
  pendingBadge,
  portsBadge,
  postureBadge,
  postureTooltip,
  contingencyDepthBadge,
  sshBadge,
  thermalBadge,
} from './CruciblePage';

// ── Mocks ─────────────────────────────────────────────────────────────────

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

vi.mock('../context/MatrixRainContext', () => ({
  useMatrixRain: () => ({ setCrucibleFocus: vi.fn() }),
}));

vi.mock('../hooks/useFleetGroups', () => ({
  useFleetGroups: () => ({ groups: [], addGroup: vi.fn(), removeGroup: vi.fn() }),
}));

vi.mock('../api/client', () => ({
  api: {
    sendAgentCommand: vi.fn().mockResolvedValue({ success: true }),
  },
}));

vi.mock('../components/Fleet/CrucibleExpandedOps', () => ({
  default: () => <div data-testid="expanded-ops" />,
}));

vi.mock('../components/Fleet/FleetHeatMiniMap', () => ({
  default: () => <div data-testid="heat-map" />,
}));

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

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

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

vi.mock('../hooks/useFleetBulkActions', () => ({
  useFleetBulkActions: () => ({ bulkBusy: false, handleBulkAction: vi.fn() }),
}));

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

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

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

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

vi.mock('../components/NeonCard/NeonCard', () => ({
  default: ({ children, className }: { children: React.ReactNode; className?: string }) => (
    <div className={className}>{children}</div>
  ),
}));

const useWebSocketMock = vi.mocked(useWebSocket);

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

function renderCrucible(wsValue: ReturnType<typeof makeWsValue>) {
  useWebSocketMock.mockReturnValue(wsValue as ReturnType<typeof useWebSocket>);
  return render(
    <MemoryRouter>
      <CruciblePage />
    </MemoryRouter>
  );
}

// ── Terminal rendering tests ──────────────────────────────────────────────

describe('CruciblePage terminal — command_result processing', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

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

  it('displays exec result in terminal when agent is selected', async () => {
    const agent = mockAgent({ id: 'agent-aaa-001', name: 'TestNode', status: 'online' });
    const commandResults = [
      { agent_id: 'agent-aaa-001', action: 'exec', success: true, message: 'hello world', _seq: 1 },
    ];

    // Render with the agent already "selected" by providing a pre-selected state.
    // The component reads commandResults from context and renders terminal lines.
    renderCrucible(makeWsValue({ agents: [agent], commandResults }));

    // The terminal should display the result message from the command.
    await waitFor(() => {
      expect(screen.getByText('hello world')).toBeInTheDocument();
    });
  });

  it('displays multiple exec result lines split on newline', async () => {
    const agent = mockAgent({ id: 'agent-bbb-002', name: 'MultiNode', status: 'online' });
    const commandResults = [
      {
        agent_id: 'agent-bbb-002',
        action: 'exec',
        success: true,
        message: 'line one\nline two\nline three',
        _seq: 1,
      },
    ];

    renderCrucible(makeWsValue({ agents: [agent], commandResults }));

    await waitFor(() => {
      expect(screen.getByText('line one')).toBeInTheDocument();
      expect(screen.getByText('line two')).toBeInTheDocument();
      expect(screen.getByText('line three')).toBeInTheDocument();
    });
  });

  it('skips command_result entries with missing agent_id', async () => {
    const commandResults = [
      { agent_id: undefined, action: 'exec', success: true, message: 'ghost output', _seq: 1 },
    ];

    renderCrucible(makeWsValue({ commandResults }));

    // "ghost output" should not appear — entry has no agent_id so it's skipped.
    await new Promise((r) => setTimeout(r, 50));
    expect(screen.queryByText('ghost output')).not.toBeInTheDocument();
  });

  it('skips already-seen entries when new commandResults arrive', async () => {
    const agent = mockAgent({ id: 'agent-ccc-003', name: 'SeqNode', status: 'online' });

    // Initial render: seq=1
    const ws1 = makeWsValue({
      agents: [agent],
      commandResults: [{ agent_id: 'agent-ccc-003', action: 'exec', success: true, message: 'first', _seq: 1 }],
    });
    const { rerender } = renderCrucible(ws1);

    await waitFor(() => expect(screen.getByText('first')).toBeInTheDocument());

    // Update: add seq=2, keep seq=1 — only 'second' should be added (not 'first' again)
    const ws2 = makeWsValue({
      agents: [agent],
      commandResults: [
        { agent_id: 'agent-ccc-003', action: 'exec', success: true, message: 'first', _seq: 1 },
        { agent_id: 'agent-ccc-003', action: 'exec', success: true, message: 'second', _seq: 2 },
      ],
    });
    useWebSocketMock.mockReturnValue(ws2 as ReturnType<typeof useWebSocket>);
    rerender(
      <MemoryRouter>
        <CruciblePage />
      </MemoryRouter>
    );

    await waitFor(() => expect(screen.getByText('second')).toBeInTheDocument());
    // 'first' should appear exactly once (not twice from a replay)
    expect(screen.getAllByText('first')).toHaveLength(1);
  });

  it('renders fleet filter toolbar when agents are registered', async () => {
    const agent = mockAgent({ name: 'FilterNode', status: 'online' });
    renderCrucible(makeWsValue({ agents: [agent] }));
    await waitFor(() => {
      expect(screen.getByPlaceholderText('Search name, IP, notes, tags…')).toBeInTheDocument();
    });
    expect(screen.getByText('FilterNode')).toBeInTheDocument();
  });

  it('shows filter empty hint when no nodes match', async () => {
    const agent = mockAgent({ name: 'HiddenNode', tags: ['prod'] });
    renderCrucible(makeWsValue({ agents: [agent] }));
    await waitFor(() => expect(screen.getByPlaceholderText('Search name, IP, notes, tags…')).toBeInTheDocument());
    await userEvent.setup().type(screen.getByPlaceholderText('Search name, IP, notes, tags…'), 'nomatchxyz');
    expect(screen.getByText('No nodes match filters.')).toBeInTheDocument();
  });

  it('shows fallback line when command result message is empty', async () => {
    const agent = mockAgent({ id: 'agent-empty-001', name: 'EmptyNode', status: 'online' });
    renderCrucible(
      makeWsValue({
        agents: [agent],
        commandResults: [
          { agent_id: 'agent-empty-001', action: 'pause', success: true, message: '', _seq: 1 },
        ],
      }),
    );

    await waitFor(() => {
      expect(screen.getByText('[pause] OK')).toBeInTheDocument();
    });
  });

  it('renders mining diagnostics JSON in terminal', async () => {
    const agent = mockAgent({ id: 'agent-diag-001', name: 'DiagNode', status: 'online' });
    const diagnostics = {
      generated_at: '2026-06-06T12:00:00.000Z',
      platform: 'windows',
      configured_execution: 'auto',
      execution_mode: 'inprocess',
      active_method: 'inprocess',
      lotl_tier: 'inprocess',
      mining_hashrate: 512,
      lotl_attempts: [
        { tier: 'container', ok: false, error: 'docker not found', duration_ms: 600 },
        { tier: 'inprocess', ok: true, duration_ms: 1800 },
      ],
      likely_blockers: [
        'mining paused by remote command or healthy container delegation',
        'Windows Defender real-time protection is ON — use Calibrate exclusion script or allowlist install path',
      ],
      cpu: { remote_paused: true, has_job: false, hashrate_hps: 0 },
      gpu: { enabled: false, active: false, paused: false },
    };
    const commandResults = [
      {
        agent_id: 'agent-diag-001',
        action: 'mining_diagnostics',
        success: true,
        message: JSON.stringify(diagnostics, null, 2),
        _seq: 1,
      },
    ];

    renderCrucible(makeWsValue({ agents: [agent], commandResults }));

    await waitFor(() => {
      expect(screen.getByText('MINING DIAGNOSTICS')).toBeInTheDocument();
      expect(screen.getByText('LIKELY BLOCKERS')).toBeInTheDocument();
      expect(
        screen.getByText('mining paused by remote command or healthy container delegation'),
      ).toBeInTheDocument();
      expect(
        screen.getByText(
          'Windows Defender real-time protection is ON — use Calibrate exclusion script or allowlist install path',
        ),
      ).toBeInTheDocument();
      expect(screen.getByText('Execution')).toBeInTheDocument();
      expect(screen.getAllByText('inprocess').length).toBeGreaterThanOrEqual(1);
      expect(screen.getByText('LOTL TIER CHAIN')).toBeInTheDocument();
      expect(screen.getByText('docker not found')).toBeInTheDocument();
      expect(screen.getByText('600ms')).toBeInTheDocument();
    });
  });

  it('shows LOTL tier badge on agent card when lotl_tier is set', async () => {
    const agent = mockAgent({
      id: 'agent-lotl-001',
      name: 'LotlNode',
      status: 'online',
      lotl_tier: 'wsl',
    });
    renderCrucible(makeWsValue({ agents: [agent] }));
    await waitFor(() => {
      expect(screen.getByText('LOTL WSL')).toBeInTheDocument();
    });
  });
});

// ── Helper function tests ─────────────────────────────────────────────────

describe('CruciblePage helpers', () => {
  it('agentColor cycles palette by agent order', () => {
    const ids = ['a', 'b', 'c'];
    expect(agentColor('a', ids)).toBe('#00e8f5');
    expect(agentColor('b', ids)).toBe('#39ff14');
    expect(agentColor('missing', ids)).toBe('#00e8f5');
  });

  it('sshBadge reflects ssh_available tri-state', () => {
    expect(sshBadge(mockAgent({ ssh_available: true })).label).toBe('SSH ON');
    expect(sshBadge(mockAgent({ ssh_available: false })).cls).toBe('ssh-off');
    expect(sshBadge(mockAgent({ ssh_available: undefined })).label).toBe('SSH ?');
  });

  it('postureBadge buckets score thresholds', () => {
    expect(postureBadge(undefined).cls).toBe('posture-unk');
    expect(postureBadge(90).cls).toBe('posture-good');
    expect(postureBadge(50).cls).toBe('posture-warn');
    expect(postureBadge(10).cls).toBe('posture-bad');
  });

  it('patchLabel marks stale patches beyond 30 days', () => {
    expect(patchLabel(10)?.cls).toBe('patch-ok');
    expect(patchLabel(45)?.cls).toBe('patch-stale');
    expect(patchLabel(undefined)).toBeNull();
  });

  it('portsBadge flags high listener counts', () => {
    expect(portsBadge(5)?.cls).toBe('ports-ok');
    expect(portsBadge(25)?.cls).toBe('ports-many');
  });

  it('pendingBadge encodes update counts', () => {
    expect(pendingBadge(mockAgent({ pending_updates: 0 }))?.label).toBe('UP TO DATE');
    expect(pendingBadge(mockAgent({ pending_updates: 3 }))?.cls).toBe('upd-warn');
    expect(pendingBadge(mockAgent({ pending_updates: 12 }))?.cls).toBe('upd-bad');
    expect(pendingBadge(mockAgent({ pending_updates: -1 }))?.label).toBe('UPD ?');
  });

  it('thermalBadge shows hot and warm thresholds', () => {
    expect(thermalBadge(mockAgent({ cpu_temp_c: 60 }))).toBeNull();
    expect(thermalBadge(mockAgent({ cpu_temp_c: 70 }))?.cls).toBe('therm-warm');
    expect(thermalBadge(mockAgent({ gpu_temp_c: 85 }))?.cls).toBe('therm-hot');
  });

  it('contingencyDepthBadge shows ONION depth when present', () => {
    expect(contingencyDepthBadge(mockAgent({ contingency_depth: 0 }))).toBeNull();
    expect(contingencyDepthBadge(mockAgent({ contingency_depth: 3 }))?.label).toBe('ONION 3');
    expect(contingencyDepthBadge(mockAgent({ contingency_depth: 10 }))?.cls).toBe('cn-contingency-deep');
  });

  it('postureTooltip includes defender, DNS drift, and services', () => {
    const agent = mockAgent({
      defender_enabled: true,
      defender_rtp: false,
      dns_servers: ['8.8.8.8'],
      dns_drifted: true,
      services: [{ name: 'sshd', display_name: 'OpenSSH', status: 'running', start_type: 'auto' }],
    });
    const tip = postureTooltip(agent);
    expect(tip).toContain('Defender');
    expect(tip).toContain('DNS changed since last heartbeat');
    expect(tip).toContain('OpenSSH');
  });
});
