import { render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import App, { copyTextToClipboard } from './App';
import { fallbackDashboardSnapshot } from './data/mockDashboard';
import { MockWebSocket } from './test/setup';

async function loginIntoPanel(user: ReturnType<typeof userEvent.setup>) {
  await user.type(screen.getByLabelText(/login/i), 'admin');
  await user.type(screen.getByLabelText(/password/i), 'proxy-ui-demo');
  await user.click(screen.getByRole('button', { name: /open panel/i }));
}

function mockClipboardWriteText(writeText: ReturnType<typeof vi.fn>) {
  Object.defineProperty(navigator, 'clipboard', {
    configurable: true,
    value: {
      writeText,
    },
  });
}

beforeEach(() => {
  document.documentElement.dataset.theme = '';
  window.sessionStorage.clear();
  window.localStorage.clear();
  window.history.replaceState(null, '', '/');
});

describe('App login gate', () => {
  it('rejects wrong hardcoded credentials and keeps the panel locked', async () => {
    const user = userEvent.setup();
    render(<App />);

    await user.type(screen.getByLabelText(/login/i), 'admin');
    await user.type(screen.getByLabelText(/password/i), 'wrong-pass');
    await user.click(screen.getByRole('button', { name: /open panel/i }));

    expect(screen.getByText(/wrong panel credentials/i)).toBeInTheDocument();
    expect(screen.queryByRole('navigation', { name: /primary/i })).not.toBeInTheDocument();
  });

  it('unlocks the panel with the configured credentials', async () => {
    const user = userEvent.setup();
    render(<App />);

    await loginIntoPanel(user);

    expect(screen.getByRole('navigation', { name: /primary/i })).toBeInTheDocument();
    expect(screen.getByRole('heading', { name: /3proxy ui/i })).toBeInTheDocument();
  });

  it('restores the panel session from sessionStorage after a remount', async () => {
    const user = userEvent.setup();
    const firstRender = render(<App />);

    await loginIntoPanel(user);
    expect(screen.getByRole('navigation', { name: /primary/i })).toBeInTheDocument();

    firstRender.unmount();
    render(<App />);

    expect(screen.getByRole('navigation', { name: /primary/i })).toBeInTheDocument();
    expect(screen.queryByRole('button', { name: /open panel/i })).not.toBeInTheDocument();
  });

  it('stores panel language in localStorage and restores it after a remount', async () => {
    const user = userEvent.setup();
    const firstRender = render(<App />);

    await loginIntoPanel(user);
    await user.click(screen.getByRole('button', { name: /settings/i }));
    await user.selectOptions(screen.getByLabelText(/panel language/i), 'ru');

    expect(screen.getByRole('button', { name: /панель/i })).toBeInTheDocument();

    firstRender.unmount();
    render(<App />);

    expect(screen.getByRole('button', { name: /панель/i })).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /^настройки$/i })).toBeInTheDocument();
  });

  it('stores panel theme in localStorage and restores it after a remount', async () => {
    const user = userEvent.setup();
    const firstRender = render(<App />);

    await loginIntoPanel(user);
    await user.click(screen.getByRole('button', { name: /settings/i }));
    await user.selectOptions(screen.getByLabelText(/panel style/i), 'dark');

    expect(document.documentElement.dataset.theme).toBe('dark');

    firstRender.unmount();
    render(<App />);

    expect(document.documentElement.dataset.theme).toBe('dark');
  });

  it('keeps tab navigation in the hash and restores the active tab after remount', async () => {
    const user = userEvent.setup();
    const firstRender = render(<App />);

    await loginIntoPanel(user);
    await user.click(screen.getByRole('button', { name: /users/i }));

    expect(window.location.hash).toBe('#users');
    expect(screen.getByRole('button', { name: /new user/i })).toBeInTheDocument();

    firstRender.unmount();
    render(<App />);

    expect(window.location.hash).toBe('#users');
    expect(screen.getByRole('button', { name: /new user/i })).toBeInTheDocument();
  });

  it('can stop the runtime from the dashboard', async () => {
    const user = userEvent.setup();
    render(<App />);

    await loginIntoPanel(user);
    await user.click(screen.getByRole('button', { name: /stop/i }));

    expect(screen.getAllByText(/^idle$/i).length).toBeGreaterThan(0);
  });

  it('shows online data in the users table and keeps proxy copy inside actions', async () => {
    const user = userEvent.setup();
    render(<App />);

    await loginIntoPanel(user);
    await user.click(screen.getByRole('button', { name: /users/i }));

    expect(screen.getByRole('columnheader', { name: /online/i })).toBeInTheDocument();
    expect(screen.queryByRole('columnheader', { name: /^proxy$/i })).not.toBeInTheDocument();
    expect(screen.getAllByRole('button', { name: /copy proxy link/i }).length).toBeGreaterThan(0);
    expect(screen.getAllByText(/^now$/i).length).toBeGreaterThan(0);
  });

  it('copies the proxy link via the Clipboard API when available', async () => {
    const clipboardWriteText = vi.fn().mockResolvedValue(undefined);
    mockClipboardWriteText(clipboardWriteText);

    await copyTextToClipboard('socks5://night-shift:kettle!23@edge.example.net:1080');

    expect(clipboardWriteText).toHaveBeenCalledWith('socks5://night-shift:kettle!23@edge.example.net:1080');
  });

  it('falls back to execCommand copy when the Clipboard API is unavailable', async () => {
    Object.defineProperty(navigator, 'clipboard', {
      configurable: true,
      value: undefined,
    });
    const execCommand = vi.fn(() => true);
    Object.defineProperty(document, 'execCommand', {
      configurable: true,
      value: execCommand,
    });

    await copyTextToClipboard('socks5://night-shift:kettle!23@edge.example.net:1080');

    expect(execCommand).toHaveBeenCalledWith('copy');
  });

  it('opens add-user flow in a modal and closes it on escape', async () => {
    const user = userEvent.setup();
    render(<App />);

    await loginIntoPanel(user);
    await user.click(screen.getByRole('button', { name: /users/i }));
    await user.click(screen.getByRole('button', { name: /new user/i }));

    const dialog = screen.getByRole('dialog', { name: /add user/i });

    expect(dialog).toBeInTheDocument();
    expect(String((screen.getByLabelText(/username/i) as HTMLInputElement).value)).toMatch(/^user-/i);
    expect(String((screen.getByLabelText(/password/i) as HTMLInputElement).value)).toMatch(/^pw-/i);
    expect(screen.getByLabelText(/service/i)).toBeInTheDocument();
    expect(screen.queryByLabelText(/port/i)).not.toBeInTheDocument();
    expect(within(dialog).getByText(/edge\.example\.net:1080/i)).toBeInTheDocument();

    await user.keyboard('{Escape}');

    expect(screen.queryByRole('dialog', { name: /add user/i })).not.toBeInTheDocument();
  });

  it('pauses and resumes a user profile from the table', async () => {
    const user = userEvent.setup();
    render(<App />);

    await loginIntoPanel(user);
    await user.click(screen.getByRole('button', { name: /users/i }));

    await user.click(screen.getAllByRole('button', { name: /pause user/i })[0]);
    expect(screen.getByText(/^paused$/i)).toBeInTheDocument();
    expect(screen.getAllByRole('button', { name: /resume user/i })).toHaveLength(1);

    await user.click(screen.getByRole('button', { name: /resume user/i }));
    expect(screen.queryByText(/^paused$/i)).not.toBeInTheDocument();
  });

  it('confirms before deleting a user and removes the row after approval', async () => {
    const user = userEvent.setup();
    render(<App />);

    await loginIntoPanel(user);
    await user.click(screen.getByRole('button', { name: /users/i }));

    expect(screen.getByText(/night-shift/i)).toBeInTheDocument();

    await user.click(screen.getAllByRole('button', { name: /^delete user$/i })[0]);

    const dialog = screen.getByRole('dialog', { name: /delete user/i });
    expect(dialog).toBeInTheDocument();
    expect(within(dialog).getByText(/remove profile/i)).toBeInTheDocument();

    await user.click(within(dialog).getByRole('button', { name: /delete user/i }));

    expect(screen.queryByText(/night-shift/i)).not.toBeInTheDocument();
    expect(screen.queryByRole('dialog', { name: /delete user/i })).not.toBeInTheDocument();
  });

  it('uses a combined service type field in settings and applies saved proxy endpoint values to the local fallback state', async () => {
    const user = userEvent.setup();
    render(<App />);

    await loginIntoPanel(user);
    await user.click(screen.getByRole('button', { name: /settings/i }));

    expect(screen.getByRole('heading', { name: /panel settings/i })).toBeInTheDocument();
    expect(screen.getByRole('heading', { name: /^services$/i })).toBeInTheDocument();
    expect(screen.getByLabelText(/proxy endpoint/i)).toBeInTheDocument();
    expect(screen.queryByLabelText(/command/i)).not.toBeInTheDocument();
    expect(screen.queryByLabelText(/protocol/i)).not.toBeInTheDocument();
    expect(screen.getAllByLabelText(/type/i)).toHaveLength(3);
    expect(screen.queryByRole('option', { name: /admin/i })).not.toBeInTheDocument();

    await user.clear(screen.getByLabelText(/proxy endpoint/i));
    await user.type(screen.getByLabelText(/proxy endpoint/i), 'gw.example.net');

    const firstPortInput = screen.getAllByLabelText(/port/i)[0];
    await user.clear(firstPortInput);
    await user.type(firstPortInput, '1180');

    await user.click(screen.getByRole('button', { name: /save settings/i }));
    await user.click(screen.getByRole('button', { name: /users/i }));

    expect(screen.getAllByText(/gw\.example\.net:1180/i).length).toBeGreaterThan(0);
  });

  it('opens edit-user flow with existing credentials instead of generating new ones', async () => {
    const user = userEvent.setup();
    render(<App />);

    await loginIntoPanel(user);
    await user.click(screen.getByRole('button', { name: /users/i }));
    await user.click(screen.getAllByRole('button', { name: /edit user/i })[0]);

    const dialog = screen.getByRole('dialog', { name: /edit user/i });

    expect(within(dialog).getByLabelText(/username/i)).toHaveValue('night-shift');
    expect(within(dialog).getByLabelText(/password/i)).toHaveValue('kettle!23');

    await user.clear(within(dialog).getByLabelText(/username/i));
    await user.type(within(dialog).getByLabelText(/username/i), 'night-shift-updated');
    await user.click(within(dialog).getByRole('button', { name: /save user/i }));

    expect(screen.getByText(/night-shift-updated/i)).toBeInTheDocument();
    expect(screen.queryByRole('dialog', { name: /edit user/i })).not.toBeInTheDocument();
  });

  it('does not overwrite dirty system settings when a websocket patch arrives', async () => {
    const user = userEvent.setup();
    render(<App />);

    await loginIntoPanel(user);
    await waitFor(() => expect(MockWebSocket.instances.length).toBeGreaterThan(0));
    const socket = MockWebSocket.instances[0];

    await user.click(screen.getByRole('button', { name: /settings/i }));

    const endpointInput = screen.getByLabelText(/proxy endpoint/i);
    await user.clear(endpointInput);
    await user.type(endpointInput, 'draft.example.net');

    socket.emitMessage({
      type: 'snapshot.patch',
      patch: {
        system: {
          ...fallbackDashboardSnapshot.system,
          publicHost: 'server-sync.example.net',
        },
      },
    });

    expect(screen.getByLabelText(/proxy endpoint/i)).toHaveValue('draft.example.net');
  });

  it('shows a connection notice and exits the panel after websocket and api retries are exhausted', async () => {
    vi.useFakeTimers();
    MockWebSocket.connectMode = 'close';
    window.sessionStorage.setItem(
      '3proxy-ui-panel-session',
      JSON.stringify({
        token: 'local-ui-fallback',
        expiresAt: new Date(Date.now() + 60_000).toISOString(),
      }),
    );

    render(<App />);
    await vi.advanceTimersByTimeAsync(2_100);

    expect(screen.getByText(/reconnecting websocket|переподключаем websocket/i)).toBeInTheDocument();

    await vi.advanceTimersByTimeAsync(25_000);

    expect(screen.getByRole('button', { name: /open panel/i })).toBeInTheDocument();
  }, 10_000);

  it('warns before deleting a service and removes linked users after confirmation', async () => {
    const user = userEvent.setup();
    render(<App />);

    await loginIntoPanel(user);
    await user.click(screen.getByRole('button', { name: /settings/i }));

    await user.click(screen.getAllByRole('button', { name: /^remove$/i })[0]);

    const dialog = screen.getByRole('dialog', { name: /delete service/i });
    expect(dialog).toBeInTheDocument();
    expect(within(dialog).getByText(/linked users to be removed/i)).toBeInTheDocument();
    expect(within(dialog).getByText(/night-shift, ops-east/i)).toBeInTheDocument();

    await user.click(within(dialog).getByRole('button', { name: /^remove$/i }));
    await user.click(screen.getByRole('button', { name: /save settings/i }));
    await user.click(screen.getByRole('button', { name: /users/i }));

    expect(screen.queryByText(/night-shift/i)).not.toBeInTheDocument();
    expect(screen.queryByText(/ops-east/i)).not.toBeInTheDocument();
  });
});
