import {
  describe,
  it,
  expect,
  vi,
  beforeEach
} from 'vitest';
import {
  Injector,
  runInInjectionContext,
  signal
} from '@angular/core';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { of, throwError } from 'rxjs';

import { ServerBrowserComponent } from './server-browser.component';
import { RoomsActions } from '../../../../store/rooms/rooms.actions';
import { selectSearchResults, selectSavedRooms } from '../../../../store/rooms/rooms.selectors';
import { selectCurrentUser } from '../../../../store/users/users.selectors';
import { ExternalLinkService } from '../../../../core/platform';
import { DatabaseService } from '../../../../infrastructure/persistence';
import { ServerDirectoryFacade } from '../../application/facades/server-directory.facade';
import { RealtimeSessionFacade } from '../../../../core/realtime';
import { PluginRequirementService, PluginStoreService } from '../../../plugins';
import { initializeAppI18nForTests, provideAppI18nForTests } from '../../../../core/i18n/app-i18n.testing';
import type { ServerInfo } from '../../domain/models/server-directory.model';
import type { User } from '../../../../shared-kernel';

interface HarnessOptions {
  joinResult?: unknown;
  joinError?: unknown;
  installedPluginIds?: Set<string>;
  snapshotRequirements?: unknown[];
}

const TEST_USER: User = {
  id: 'user-1',
  oderId: 'oder-1',
  displayName: 'Tester'
} as unknown as User;
const TEST_SERVER: ServerInfo = {
  id: 'server-1',
  name: 'Alpha',
  maxUsers: 50,
  userCount: 3,
  isPrivate: false
} as unknown as ServerInfo;

function createHarness(options: HarnessOptions = {}) {
  const dispatch = vi.fn();
  const searchResultsSig = signal<ServerInfo[]>([]);
  const savedRoomsSig = signal<unknown[]>([]);
  const currentUserSig = signal<User | null>(TEST_USER);
  const store = {
    selectSignal: (selector: unknown) => {
      if (selector === selectSearchResults) {
        return searchResultsSig;
      }

      if (selector === selectSavedRooms) {
        return savedRoomsSig;
      }

      if (selector === selectCurrentUser) {
        return currentUserSig;
      }

      return signal(null);
    },
    dispatch
  } as unknown as Store;
  const router = { navigate: vi.fn() } as unknown as Router;
  const requestJoin = vi.fn(() =>
    options.joinError ? throwError(() => options.joinError) : of(options.joinResult ?? { server: { id: 'server-1' }, signalingUrl: 'wss://x' })
  );
  const serverDirectory = {
    activeServers: () => [],
    requestJoin,
    normaliseRoomSignalSource: () => ({}),
    getApiBaseUrl: () => 'https://api.test',
    buildRoomSignalSelector: () => null,
    getWebSocketUrl: () => 'wss://x'
  } as unknown as ServerDirectoryFacade;
  const pluginRequirements = {
    getSnapshot: vi.fn(() => of({ requirements: options.snapshotRequirements ?? [] }))
  } as unknown as PluginRequirementService;
  const installServerRequirementsLocally = vi.fn(() => Promise.resolve());
  const pluginStore = {
    getLocalServerInstalledPluginIds: vi.fn(() => Promise.resolve(options.installedPluginIds ?? new Set<string>())),
    installServerRequirementsLocally,
    loadRequirementReadme: vi.fn(() => Promise.resolve({ title: 'x', markdown: '' }))
  } as unknown as PluginStoreService;
  const db = {
    getBansForRoom: vi.fn(() => Promise.resolve([]))
  } as unknown as DatabaseService;
  const webrtc = {
    connectToSignalingServer: vi.fn(() => of(undefined)),
    identify: vi.fn(),
    sendRawMessageToSignalUrl: vi.fn()
  } as unknown as RealtimeSessionFacade;
  const externalLinks = { open: vi.fn() } as unknown as ExternalLinkService;
  const injector = Injector.create({
    providers: [
      ServerBrowserComponent,
      { provide: Store, useValue: store },
      { provide: Router, useValue: router },
      { provide: DatabaseService, useValue: db },
      { provide: ExternalLinkService, useValue: externalLinks },
      { provide: ServerDirectoryFacade, useValue: serverDirectory },
      { provide: RealtimeSessionFacade, useValue: webrtc },
      { provide: PluginRequirementService, useValue: pluginRequirements },
      { provide: PluginStoreService, useValue: pluginStore },
      ...provideAppI18nForTests()
    ]
  });

  initializeAppI18nForTests(injector);
  const component = runInInjectionContext(injector, () => injector.get(ServerBrowserComponent));

  return { component, dispatch, requestJoin, router, installServerRequirementsLocally };
}

function installLocalStorageMock(): void {
  const store = new Map<string, string>();

  vi.stubGlobal('localStorage', {
    getItem: (key: string) => store.get(key) ?? null,
    setItem: (key: string, value: string) => store.set(key, String(value)),
    removeItem: (key: string) => store.delete(key),
    clear: () => store.clear(),
    key: (index: number) => Array.from(store.keys())[index] ?? null,
    get length() {
      return store.size;
    }
  });
}

describe('ServerBrowserComponent join flow', () => {
  beforeEach(() => {
    installLocalStorageMock();
    localStorage.setItem('metoyou_currentUserId', 'user-1');
  });

  it('dispatches joinRoom after a successful join with no plugin requirements', async () => {
    const { component, dispatch } = createHarness({
      joinResult: { server: { id: 'server-1', name: 'Alpha' }, signalingUrl: 'wss://x' }
    });

    await component.joinServer(TEST_SERVER);

    const joinDispatch = dispatch.mock.calls.find(([action]) => action.type === RoomsActions.joinRoom.type);

    expect(joinDispatch).toBeTruthy();
    expect(joinDispatch?.[0].roomId).toBe('server-1');
  });

  it('opens the password dialog when the server requires a password', async () => {
    const { component, dispatch } = createHarness({
      joinError: { error: { errorCode: 'PASSWORD_REQUIRED', error: 'Password required' } }
    });

    await component.joinServer(TEST_SERVER);

    expect(component.showPasswordDialog()).toBe(true);
    expect(component.passwordPromptServer()?.id).toBe('server-1');
    expect(dispatch.mock.calls.some(([action]) => action.type === RoomsActions.joinRoom.type)).toBe(false);
  });

  it('shows the banned dialog when the server reports the user is banned', async () => {
    const { component } = createHarness({
      joinError: { error: { errorCode: 'BANNED', error: 'Banned' } }
    });

    await component.joinServer(TEST_SERVER);

    expect(component.showBannedDialog()).toBe(true);
    expect(component.bannedServerName()).toBe('Alpha');
  });

  it('presents a plugin-consent dialog before joining when requirements exist', async () => {
    const { component, dispatch } = createHarness({
      snapshotRequirements: [{ pluginId: 'p1', status: 'required', manifest: { title: 'P1' } }]
    });

    await component.joinServer(TEST_SERVER);

    expect(component.pluginConsentDialog()).toBeTruthy();
    expect(component.pluginConsentDialog()?.required).toHaveLength(1);
    expect(dispatch.mock.calls.some(([action]) => action.type === RoomsActions.joinRoom.type)).toBe(false);
  });

  it('installs accepted requirements then joins on consent confirmation', async () => {
    const { component, dispatch, installServerRequirementsLocally } = createHarness({
      snapshotRequirements: [{ pluginId: 'p1', status: 'required', manifest: { title: 'P1' } }],
      joinResult: { server: { id: 'server-1', name: 'Alpha' }, signalingUrl: 'wss://x' }
    });

    await component.joinServer(TEST_SERVER);
    await component.confirmPluginConsent();

    expect(installServerRequirementsLocally).toHaveBeenCalled();
    expect(dispatch.mock.calls.some(([action]) => action.type === RoomsActions.joinRoom.type)).toBe(true);
  });

  it('hides discovery sections that have no servers', () => {
    const { component } = createHarness();

    component.discoverySections = [{ id: 'a', title: 'A', servers: [] }, { id: 'b', title: 'B', servers: [TEST_SERVER] }];

    expect(component.visibleSections).toHaveLength(1);
    expect(component.visibleSections[0].id).toBe('b');
    expect(component.showEmptyState).toBe(false);
  });

  it('reports an empty state when no sections have servers and not searching', () => {
    const { component } = createHarness();

    component.discoverySections = [{ id: 'a', title: 'A', servers: [] }];

    expect(component.showEmptyState).toBe(true);
  });
});
