import { describe, expect, test, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
import ActiveSessions from './ActiveSessions.svelte';

const listSessions = vi.fn();
const revokeSession = vi.fn();
const revokeOtherSessions = vi.fn();

vi.mock('$lib/api/me', () => ({
  listSessions: (...a: unknown[]) => listSessions(...a),
  revokeSession: (...a: unknown[]) => revokeSession(...a),
  revokeOtherSessions: (...a: unknown[]) => revokeOtherSessions(...a)
}));

vi.mock('$lib/stores/toast.svelte', () => ({ pushToast: vi.fn() }));

type Row = {
  id: string;
  user_agent: string;
  created_ip: string;
  last_ip: string;
  created_at: string;
  last_seen_at: string;
  current: boolean;
};

function row(over: Partial<Row> = {}): Row {
  return {
    id: 'a1',
    user_agent: 'Mozilla/5.0 (X11; Linux x86_64) Chrome/120.0',
    created_ip: '203.0.113.1',
    last_ip: '203.0.113.1',
    created_at: new Date().toISOString(),
    last_seen_at: new Date().toISOString(),
    current: false,
    ...over
  };
}

beforeEach(() => {
  vi.clearAllMocks();
});

describe('ActiveSessions', () => {
  test('flags the current session and gives it no sign-out button', async () => {
    listSessions.mockResolvedValue([
      row({ id: 'cur', current: true }),
      row({ id: 'other', current: false })
    ]);
    render(ActiveSessions);

    await screen.findByText('This device');
    // One sign-out button, for the non-current row. Offering one on the
    // current session would sign the user out of the page they're using.
    await waitFor(() => {
      expect(screen.getAllByRole('button', { name: 'Sign out' })).toHaveLength(1);
    });
  });

  // The whole reason IP is stored: surfacing the mismatch rather than making
  // someone compare two addresses by eye.
  test('warns when a session is used from a different address than it was created', async () => {
    listSessions.mockResolvedValue([
      row({ id: 'moved', created_ip: '203.0.113.1', last_ip: '198.51.100.9' })
    ]);
    render(ActiveSessions);

    expect(await screen.findByText('Address changed')).toBeTruthy();
  });

  test('does not warn when the address has not changed', async () => {
    listSessions.mockResolvedValue([
      row({ created_ip: '203.0.113.1', last_ip: '203.0.113.1' })
    ]);
    render(ActiveSessions);

    await screen.findByText(/Signed in/);
    expect(screen.queryByText('Address changed')).toBeNull();
  });

  test('sign-out-all-others confirms before acting', async () => {
    listSessions.mockResolvedValue([
      row({ id: 'cur', current: true }),
      row({ id: 'o1' }),
      row({ id: 'o2' })
    ]);
    revokeOtherSessions.mockResolvedValue(2);
    render(ActiveSessions);

    const start = await screen.findByRole('button', { name: 'Sign out all other devices' });
    await fireEvent.click(start);
    // First click only arms the action.
    expect(revokeOtherSessions).not.toHaveBeenCalled();
    expect(screen.getByText(/Sign out 2 other devices\?/)).toBeTruthy();

    await fireEvent.click(screen.getByRole('button', { name: 'Sign them out' }));
    await waitFor(() => expect(revokeOtherSessions).toHaveBeenCalledTimes(1));
  });

  test('offers no bulk action when there are no other devices', async () => {
    listSessions.mockResolvedValue([row({ id: 'cur', current: true })]);
    render(ActiveSessions);

    await screen.findByText('This device');
    expect(screen.queryByRole('button', { name: 'Sign out all other devices' })).toBeNull();
  });

  test('surfaces a retry when loading fails', async () => {
    listSessions.mockRejectedValue(new Error('boom'));
    render(ActiveSessions);

    const retry = await screen.findByRole('button', { name: 'Try again' });
    listSessions.mockResolvedValue([row({ id: 'cur', current: true })]);
    await fireEvent.click(retry);
    await screen.findByText('This device');
  });

  test('renders unknown for a missing address rather than an empty cell', async () => {
    listSessions.mockResolvedValue([row({ created_ip: '', last_ip: '' })]);
    render(ActiveSessions);

    await waitFor(() => {
      expect(screen.getAllByText('unknown').length).toBeGreaterThan(0);
    });
  });
});
