import { fireEvent, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { callCoreRpc } from '../../../../services/coreRpcClient';
import { renderWithProviders } from '../../../../test/test-utils';
import DevicesPanel from '../DevicesPanel';

// ---------------------------------------------------------------------------
// Mocks
// ---------------------------------------------------------------------------

vi.mock('../../../../services/coreRpcClient', () => ({ callCoreRpc: vi.fn() }));

// qrcode.react is not needed in panel tests.
vi.mock('../devices/PairPhoneModal', () => ({
  default: ({ onClose, onPaired }: { onClose: () => void; onPaired: (id: string) => void }) => (
    <div data-testid="pair-modal">
      <button onClick={onClose}>close-modal</button>
      <button onClick={() => onPaired('CHAN123')}>simulate-paired</button>
    </div>
  ),
}));

const mockCall = vi.mocked(callCoreRpc);

function makeDevice(overrides = {}) {
  return {
    channel_id: 'CHAN_AAABBBCCC',
    label: "Alice's iPhone",
    device_pubkey: 'pubkey_base64url',
    created_at: new Date().toISOString(),
    last_seen_at: null,
    peer_online: false,
    revoked: false,
    ...overrides,
  };
}

function listResponse(devices: ReturnType<typeof makeDevice>[]) {
  return { devices };
}

describe('DevicesPanel', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('shows empty state when no devices are paired', async () => {
    mockCall.mockResolvedValue(listResponse([]));
    renderWithProviders(<DevicesPanel />, { initialEntries: ['/settings/devices'] });

    expect(await screen.findByText('No paired devices')).toBeInTheDocument();
    // Two "Pair iPhone" buttons exist: header + empty-state CTA.
    expect(screen.getAllByRole('button', { name: /Pair iPhone/i })).toHaveLength(2);
  });

  it('renders a paired device row with label, truncated id, and revoke button', async () => {
    const device = makeDevice({ channel_id: 'ABCDEFGHIJ12345678', label: "Bob's iPhone" });
    mockCall.mockResolvedValue(listResponse([device]));
    renderWithProviders(<DevicesPanel />, { initialEntries: ['/settings/devices'] });

    expect(await screen.findByText("Bob's iPhone")).toBeInTheDocument();
    // Truncated: first 4 + last 4 chars
    expect(screen.getByText('ABCD…5678')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /Revoke/i })).toBeInTheDocument();
  });

  it('filters out revoked devices', async () => {
    const devices = [
      makeDevice({ label: 'Active', revoked: false }),
      makeDevice({ channel_id: 'REVOKED_CHAN', label: 'Revoked', revoked: true }),
    ];
    mockCall.mockResolvedValue(listResponse(devices));
    renderWithProviders(<DevicesPanel />, { initialEntries: ['/settings/devices'] });

    expect(await screen.findByText('Active')).toBeInTheDocument();
    expect(screen.queryByText('Revoked')).not.toBeInTheDocument();
  });

  it('shows a confirm dialog on revoke click, then calls devices_revoke on confirm', async () => {
    const device = makeDevice({ label: "Charlie's iPhone", channel_id: 'CHAN_CHARLIE' });
    // First call: list. Second call: revoke. Third call: refresh after revoke.
    mockCall
      .mockResolvedValueOnce(listResponse([device]))
      .mockResolvedValueOnce({ success: true })
      .mockResolvedValueOnce(listResponse([]));

    renderWithProviders(<DevicesPanel />, { initialEntries: ['/settings/devices'] });

    await screen.findByText("Charlie's iPhone");
    // Click the per-row revoke button (may be many; pick the first one)
    fireEvent.click(screen.getAllByRole('button', { name: /Revoke/i })[0]);

    // Confirmation dialog
    expect(await screen.findByText('Revoke device?')).toBeInTheDocument();
    // Use testid to unambiguously target the dialog confirm button
    fireEvent.click(screen.getByTestId('confirm-revoke-btn'));

    await waitFor(() => {
      expect(mockCall).toHaveBeenCalledWith(
        expect.objectContaining({ method: 'openhuman.devices_revoke' })
      );
    });

    // After revoke the list should be refreshed (empty state)
    expect(await screen.findByText('No paired devices')).toBeInTheDocument();
  });

  it('cancels revoke when the cancel button is pressed', async () => {
    const device = makeDevice({ label: "Dave's iPhone" });
    mockCall.mockResolvedValue(listResponse([device]));
    renderWithProviders(<DevicesPanel />, { initialEntries: ['/settings/devices'] });

    await screen.findByText("Dave's iPhone");
    fireEvent.click(screen.getByRole('button', { name: /Revoke/i }));
    expect(screen.getByText('Revoke device?')).toBeInTheDocument();

    fireEvent.click(screen.getByRole('button', { name: /Cancel/i }));
    await waitFor(() => {
      expect(screen.queryByText('Revoke device?')).not.toBeInTheDocument();
    });
    // No revoke call made
    expect(mockCall).toHaveBeenCalledTimes(1);
  });

  it('opens the pair modal when Pair iPhone is clicked', async () => {
    mockCall.mockResolvedValue(listResponse([]));
    renderWithProviders(<DevicesPanel />, { initialEntries: ['/settings/devices'] });

    await screen.findByText('No paired devices');
    // Click the header-level button (first one).
    fireEvent.click(screen.getAllByRole('button', { name: /Pair iPhone/i })[0]);

    expect(await screen.findByTestId('pair-modal')).toBeInTheDocument();
  });

  it('closes the pair modal and reloads devices after pairing', async () => {
    const device = makeDevice({ label: 'New iPhone' });
    mockCall.mockResolvedValueOnce(listResponse([])).mockResolvedValueOnce(listResponse([device]));

    renderWithProviders(<DevicesPanel />, { initialEntries: ['/settings/devices'] });
    await screen.findByText('No paired devices');
    fireEvent.click(screen.getAllByRole('button', { name: /Pair iPhone/i })[0]);

    await screen.findByTestId('pair-modal');
    fireEvent.click(screen.getByText('simulate-paired'));

    await waitFor(() => {
      expect(screen.queryByTestId('pair-modal')).not.toBeInTheDocument();
    });
  });

  it('shows an error message when devices_list fails', async () => {
    mockCall.mockRejectedValue(new Error('Core offline'));
    renderWithProviders(<DevicesPanel />, { initialEntries: ['/settings/devices'] });

    expect(await screen.findByText(/Failed to load devices/)).toBeInTheDocument();
  });

  it('shows the online status indicator when a peer is online', async () => {
    const device = makeDevice({ label: 'Online iPhone', peer_online: true });
    mockCall.mockResolvedValue(listResponse([device]));
    renderWithProviders(<DevicesPanel />, { initialEntries: ['/settings/devices'] });

    await screen.findByText('Online iPhone');

    expect(screen.getByTestId('peer-status-online')).toBeInTheDocument();
    expect(screen.queryByTestId('peer-status-offline')).not.toBeInTheDocument();
  });

  it('shows the offline status indicator when a peer is offline', async () => {
    const device = makeDevice({ label: 'Offline iPhone', peer_online: false });
    mockCall.mockResolvedValue(listResponse([device]));
    renderWithProviders(<DevicesPanel />, { initialEntries: ['/settings/devices'] });

    await screen.findByText('Offline iPhone');

    expect(screen.getByTestId('peer-status-offline')).toBeInTheDocument();
    expect(screen.queryByTestId('peer-status-online')).not.toBeInTheDocument();
  });
});
