import {
  Injector,
  NgZone,
  runInInjectionContext,
  signal
} from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Store } from '@ngrx/store';
import { Subject, of } from 'rxjs';
import { ElectronBridgeService } from '../../../core/platform/electron/electron-bridge.service';
import type { ActiveGameCandidateResult } from '../../../core/platform/electron/electron-api.models';
import { RealtimeSessionFacade } from '../../../core/realtime';
import { ServerDirectoryFacade } from '../../server-directory';
import { UsersActions } from '../../../store/users/users.actions';
import { selectAllUsers, selectCurrentUser } from '../../../store/users/users.selectors';
import type {
  ChatEvent,
  GameActivity,
  GameMatchResponse,
  MatchedGame,
  User
} from '../../../shared-kernel';
import { initializeAppI18nForTests, provideAppI18nForTests } from '../../../core/i18n/app-i18n.testing';
import { GameActivityService } from './game-activity.service';

const alice = createUser('alice-id', 'alice-oder', 'Alice');
const bob = createUser('bob-id', 'bob-oder', 'Bob');
const carol = createUser('carol-id', 'carol-oder', 'Carol');

let contexts: ServiceContext[] = [];

describe('GameActivityService sync', () => {
  beforeEach(() => {
    contexts = [];
    installLocalStorageMock();
  });

  afterEach(() => {
    for (const context of contexts) {
      context.service.ngOnDestroy();
    }
  });

  it('subscribes to incoming activity on browser clients without local process scanning', () => {
    const context = createServiceContext({
      currentUser: bob,
      allUsers: [alice, bob],
      electronApi: null
    });

    context.service.start();
    context.incomingMessages.next({
      type: 'game-activity',
      fromPeerId: alice.oderId,
      oderId: alice.oderId,
      displayName: alice.displayName,
      gameActivity: createActivity('game-1', 'Deep Rock Galactic')
    } as ChatEvent);

    expect(context.store.dispatch).toHaveBeenCalledWith(UsersActions.updateGameActivity({
      userId: alice.id,
      gameActivity: createActivity('game-1', 'Deep Rock Galactic')
    }));
  });

  it('broadcasts local activity changes to peers already online', async () => {
    const matchedGame = createMatchedGame('game-2', 'Stardew Valley', 'StardewValley.exe');
    const context = createServiceContext({
      currentUser: alice,
      allUsers: [alice, bob],
      processNames: ['StardewValley.exe'],
      gameMatchResponse: { games: [matchedGame] }
    });

    context.service.start();

    await vi.waitFor(() => expect(context.realtime.broadcastMessage).toHaveBeenCalledWith(expect.objectContaining({
      type: 'game-activity',
      oderId: alice.oderId,
      displayName: alice.displayName,
      gameActivity: expect.objectContaining({
        id: matchedGame.id,
        name: matchedGame.name,
        iconUrl: matchedGame.iconUrl,
        store: matchedGame.store
      })
    })));
  });

  it('sends current activity directly to peers that connect after the status was set', async () => {
    const matchedGame = createMatchedGame('game-3', 'Hades', 'Hades.exe');
    const context = createServiceContext({
      currentUser: alice,
      allUsers: [
        alice,
        bob,
        carol
      ],
      processNames: ['Hades.exe'],
      gameMatchResponse: { games: [matchedGame] }
    });

    context.service.start();

    await vi.waitFor(() => expect(context.realtime.broadcastMessage).toHaveBeenCalled());

    context.realtime.sendToPeer.mockClear();
    context.peerConnected.next(carol.oderId);

    expect(context.realtime.sendToPeer).toHaveBeenCalledWith(carol.oderId, expect.objectContaining({
      type: 'game-activity',
      oderId: alice.oderId,
      displayName: alice.displayName,
      gameActivity: expect.objectContaining({
        id: matchedGame.id,
        name: matchedGame.name,
        iconUrl: matchedGame.iconUrl,
        store: matchedGame.store
      })
    }));
  });

  it('does not ask Electron for running games while the app window is focused', () => {
    const getActiveGameCandidate = vi.fn<() => Promise<ActiveGameCandidateResult>>(async () => ({
      candidate: null,
      fallbackProcessNames: ['Hades.exe']
    }));
    const getRunningProcessNames = vi.fn(async () => ['Hades.exe']);
    const context = createServiceContext({
      currentUser: alice,
      allUsers: [alice],
      documentHasFocus: true,
      electronApi: {
        getActiveGameCandidate,
        getRunningProcessNames
      }
    });

    context.service.start();

    expect(getActiveGameCandidate).not.toHaveBeenCalled();
    expect(getRunningProcessNames).not.toHaveBeenCalled();
  });
});

interface ServiceContextOptions {
  currentUser: User;
  allUsers: User[];
  electronApi?: {
    getActiveGameCandidate?: () => Promise<ActiveGameCandidateResult>;
    getRunningProcessNames?: () => Promise<string[]>;
  } | null;
  documentHasFocus?: boolean;
  processNames?: string[];
  gameMatchResponse?: GameMatchResponse;
}

interface ServiceContext {
  incomingMessages: Subject<ChatEvent>;
  peerConnected: Subject<string>;
  realtime: {
    broadcastMessage: ReturnType<typeof vi.fn>;
    sendToPeer: ReturnType<typeof vi.fn>;
  };
  service: GameActivityService;
  store: {
    dispatch: ReturnType<typeof vi.fn>;
  };
}

function createServiceContext(options: ServiceContextOptions): ServiceContext {
  vi.stubGlobal('document', { hasFocus: () => options.documentHasFocus ?? false });

  const currentUser = signal<User | null>(options.currentUser);
  const allUsers = signal<User[]>(options.allUsers);
  const incomingMessages = new Subject<ChatEvent>();
  const peerConnected = new Subject<string>();
  const realtime = {
    onMessageReceived: incomingMessages.asObservable(),
    onPeerConnected: peerConnected.asObservable(),
    broadcastMessage: vi.fn(),
    sendToPeer: vi.fn()
  };
  const store = {
    dispatch: vi.fn(),
    selectSignal: vi.fn((selector: unknown) => {
      if (selector === selectCurrentUser) {
        return currentUser;
      }

      if (selector === selectAllUsers) {
        return allUsers;
      }

      throw new Error('Unexpected selector requested by GameActivityService test.');
    })
  };
  const electronApi = options.electronApi === undefined
    ? { getRunningProcessNames: vi.fn(async () => options.processNames ?? []) }
    : options.electronApi;
  const injector = Injector.create({
    providers: [
      ...provideAppI18nForTests(),
      {
        provide: ElectronBridgeService,
        useValue: { getApi: () => electronApi }
      },
      {
        provide: HttpClient,
        useValue: {
          post: vi.fn(() => of(options.gameMatchResponse ?? { games: [] }))
        }
      },
      {
        provide: NgZone,
        useValue: {
          run: (fn: () => void) => fn(),
          runOutsideAngular: (fn: () => void) => fn()
        }
      },
      {
        provide: RealtimeSessionFacade,
        useValue: realtime
      },
      {
        provide: ServerDirectoryFacade,
        useValue: { getApiBaseUrl: () => 'http://localhost:3001/api' }
      },
      {
        provide: Store,
        useValue: store
      }
    ]
  });

  initializeAppI18nForTests(injector);
  const service = runInInjectionContext(injector, () => new GameActivityService());
  const context = {
    incomingMessages,
    peerConnected,
    realtime,
    service,
    store
  };

  contexts.push(context);

  return context;
}

function createUser(id: string, oderId: string, displayName: string): User {
  return {
    id,
    oderId,
    username: displayName.toLowerCase(),
    displayName,
    status: 'online',
    role: 'member',
    joinedAt: 1
  };
}

function createActivity(id: string, name: string): GameActivity {
  return {
    id,
    name,
    startedAt: 1_000
  };
}

function createMatchedGame(id: string, name: string, processName: string): MatchedGame {
  return {
    id,
    name,
    iconUrl: `https://img.example.test/${id}.jpg`,
    store: {
      name: 'Steam',
      slug: 'steam',
      url: `https://store.steampowered.com/search/?term=${encodeURIComponent(name)}`
    },
    processName
  };
}

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

  vi.stubGlobal('localStorage', {
    getItem: (key: string) => values.get(key) ?? null,
    setItem: (key: string, value: string) => values.set(key, value),
    removeItem: (key: string) => values.delete(key),
    clear: () => values.clear()
  });
}
