// @ts-nocheck
/**
 * tests/unit/foundry/FoundryAdapter.test.js
 *
 * Story 1.2 — WebRTC spike: FoundryAdapter probe tests.
 * Story 1.3 — Data layer: constructor(game), surface delegation tests.
 * Story 5.1 — Full AV Replacement: probeCapability returns 'stream-access', buildWebRTCSurface with full API
 *
 * OQ-1 spike result: css-fallback (FoundryVTT v14, 2026-05-21) - SUPERSEDED
 *   track.enabled = false does NOT stop inbound WebRTC bandwidth.
 *
 * NEW: probeCapability returns 'stream-access' when getMediaStreamForUser is available
 *   Full AV replacement architecture: hide Foundry dock, show VVM dock with actual video
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { FoundryAdapter } from '../../../src/foundry/FoundryAdapter.js';
import {
  GAME_STUB,
  SETTINGS_STUB,
  SOCKET_STUB,
  USERS_STUB,
  SCENES_STUB,
  HOOKS_STUB,
  UI_STUB,
  GM_USER,
  PLAYER_USER,
  ACTIVE_SCENE,
} from '../../fixtures/foundry-adapter.js';

// ─── Helpers ─────────────────────────────────────────────────────────────────

/**
 * Returns a minimal mock game.webrtc object whose client exposes getMediaStreamForUser.
 * @param {MediaStream|null} [stream=null]
 * @returns {{ client: { getMediaStreamForUser: import('vitest').MockedFunction<() => MediaStream|null> } }}
 */
function makeGameWebrtc(stream = null) {
  return {
    client: {
      getMediaStreamForUser: vi.fn().mockReturnValue(stream),
    },
  };
}

/**
 * Returns a minimal MediaStream mock with one video track.
 * @param {boolean} [enabled=true]
 * @returns {{ getVideoTracks: import('vitest').MockedFunction<() => object[]>, _track: { enabled: boolean, kind: string } }}
 */
function makeStream(enabled = true) {
  const track = { enabled, kind: 'video' };
  return {
    getVideoTracks: vi.fn().mockReturnValue([track]),
    _track: track,
  };
}

// ─── probeCapability ─────────────────────────────────────────────────────────

describe('FoundryAdapter.probeCapability', () => {
  it('returns "unsupported" when gameWebrtc is null', () => {
    expect(FoundryAdapter.probeCapability(null)).toBe('unsupported');
  });

  it('returns "unsupported" when gameWebrtc is undefined', () => {
    expect(FoundryAdapter.probeCapability(undefined)).toBe('unsupported');
  });

  it('returns "unsupported" when gameWebrtc has no client', () => {
    expect(FoundryAdapter.probeCapability({})).toBe('unsupported');
  });

  it('returns "unsupported" when client lacks getMediaStreamForUser', () => {
    expect(FoundryAdapter.probeCapability({ client: {} })).toBe('unsupported');
  });

  it('returns "stream-access" when client has getMediaStreamForUser (Story 5.1 - Full AV Replacement)', () => {
    // getMediaStreamForUser is available → full AV replacement is possible
    const gameWebrtc = makeGameWebrtc();
    expect(FoundryAdapter.probeCapability(gameWebrtc)).toBe('stream-access');
  });
});

// ─── buildWebRTCSurface ───────────────────────────────────────────────────────

// Helper to create a full mock WebRTC client with all methods
function makeFullWebRTCClient(stream = null) {
  return {
    getMediaStreamForUser: vi.fn().mockReturnValue(stream),
    getConnectedUsers: vi.fn().mockReturnValue(['user-1', 'user-2']),
    getLevelsStreamForUser: vi.fn().mockReturnValue(stream),
    isAudioEnabled: vi.fn().mockReturnValue(true),
    isVideoEnabled: vi.fn().mockReturnValue(true),
    toggleAudio: vi.fn(),
    toggleVideo: vi.fn(),
    toggleBroadcast: vi.fn(),
    setUserVideo: vi.fn().mockResolvedValue(undefined),
  };
}

function makeFullGameWebrtc(stream = null) {
  return {
    client: makeFullWebRTCClient(stream),
  };
}

describe('FoundryAdapter.buildWebRTCSurface', () => {
  let consoleWarnSpy;
  let consoleErrorSpy;

  beforeEach(() => {
    consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
    consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  describe('returns complete WebRTC surface with 11 methods (Story 5.1)', () => {
    it('returns object with all 11 WebRTC methods', () => {
      const gameWebrtc = makeFullGameWebrtc();
      const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);

      expect(surface).toHaveProperty('getMediaStreamForUser');
      expect(surface).toHaveProperty('getConnectedUsers');
      expect(surface).toHaveProperty('getLevelsStreamForUser');
      expect(surface).toHaveProperty('isAudioEnabled');
      expect(surface).toHaveProperty('isVideoEnabled');
      expect(surface).toHaveProperty('toggleAudio');
      expect(surface).toHaveProperty('toggleVideo');
      expect(surface).toHaveProperty('toggleBroadcast');
      expect(surface).toHaveProperty('setUserVideo');
      expect(surface).toHaveProperty('disableTrack');
      expect(surface).toHaveProperty('enableTrack');
    });

    it('getMediaStreamForUser returns stream from client', () => {
      const mockStream = new MediaStream();
      const gameWebrtc = makeFullGameWebrtc(mockStream);
      const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);

      const result = surface.getMediaStreamForUser('user-1');
      expect(result).toBe(mockStream);
      expect(gameWebrtc.client.getMediaStreamForUser).toHaveBeenCalledWith('user-1');
    });

    it('getMediaStreamForUser returns null for invalid userId', () => {
      const gameWebrtc = makeFullGameWebrtc();
      const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);

      const result = surface.getMediaStreamForUser('');
      expect(result).toBeNull();
      expect(consoleWarnSpy).toHaveBeenCalled();
    });

    it('getMediaStreamForUser returns null when client method throws', () => {
      const gameWebrtc = {
        client: {
          getMediaStreamForUser: vi.fn().mockImplementation(() => {
            throw new Error('AV error');
          }),
        },
      };
      const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);

      const result = surface.getMediaStreamForUser('user-1');
      expect(result).toBeNull();
      expect(consoleErrorSpy).toHaveBeenCalled();
    });

    it('getConnectedUsers returns array from client', () => {
      const gameWebrtc = makeFullGameWebrtc();
      const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);

      const result = surface.getConnectedUsers();
      expect(result).toEqual(['user-1', 'user-2']);
    });

    it('getConnectedUsers returns empty array on error', () => {
      const gameWebrtc = {
        client: {
          getConnectedUsers: vi.fn().mockImplementation(() => {
            throw new Error('Connection error');
          }),
        },
      };
      const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);

      const result = surface.getConnectedUsers();
      expect(result).toEqual([]);
    });

    it('isAudioEnabled delegates to client', () => {
      const gameWebrtc = makeFullGameWebrtc();
      const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);

      const result = surface.isAudioEnabled();
      expect(result).toBe(true);
      expect(gameWebrtc.client.isAudioEnabled).toHaveBeenCalled();
    });

    it('isVideoEnabled delegates to client', () => {
      const gameWebrtc = makeFullGameWebrtc();
      const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);

      const result = surface.isVideoEnabled();
      expect(result).toBe(true);
      expect(gameWebrtc.client.isVideoEnabled).toHaveBeenCalled();
    });

    it('toggleAudio calls client method with boolean', () => {
      const gameWebrtc = makeFullGameWebrtc();
      const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);

      surface.toggleAudio(true);
      expect(gameWebrtc.client.toggleAudio).toHaveBeenCalledWith(true);

      surface.toggleAudio(false);
      expect(gameWebrtc.client.toggleAudio).toHaveBeenCalledWith(false);
    });

    it('toggleVideo calls client method with boolean', () => {
      const gameWebrtc = makeFullGameWebrtc();
      const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);

      surface.toggleVideo(true);
      expect(gameWebrtc.client.toggleVideo).toHaveBeenCalledWith(true);
    });

    it('toggleBroadcast calls client method with boolean', () => {
      const gameWebrtc = makeFullGameWebrtc();
      const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);

      surface.toggleBroadcast(false);
      expect(gameWebrtc.client.toggleBroadcast).toHaveBeenCalledWith(false);
    });

    it('setUserVideo calls client method with userId and videoElement', async () => {
      const gameWebrtc = makeFullGameWebrtc();
      const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);
      const videoElement = document.createElement('video');

      await surface.setUserVideo('user-1', videoElement);
      expect(gameWebrtc.client.setUserVideo).toHaveBeenCalledWith('user-1', videoElement);
    });

    it('setUserVideo validates userId is string', async () => {
      const gameWebrtc = makeFullGameWebrtc();
      const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);
      const videoElement = document.createElement('video');

      await surface.setUserVideo(null, videoElement);
      expect(consoleWarnSpy).toHaveBeenCalled();
      expect(gameWebrtc.client.setUserVideo).not.toHaveBeenCalled();
    });

    it('setUserVideo validates videoElement is HTMLVideoElement', async () => {
      const gameWebrtc = makeFullGameWebrtc();
      const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);

      await surface.setUserVideo('user-1', {});
      expect(consoleWarnSpy).toHaveBeenCalled();
      expect(gameWebrtc.client.setUserVideo).not.toHaveBeenCalled();
    });
  });

  describe('input validation for userId-taking methods', () => {
    it('getLevelsStreamForUser warns on empty userId', () => {
      const gameWebrtc = makeFullGameWebrtc();
      const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);

      const result = surface.getLevelsStreamForUser('');
      expect(result).toBeNull();
      expect(consoleWarnSpy).toHaveBeenCalledWith(
        '[ScryingPool] getLevelsStreamForUser: invalid userId:',
        ''
      );
    });

    it('disableTrack warns on invalid userId', () => {
      const gameWebrtc = makeFullGameWebrtc();
      const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);

      surface.disableTrack(null);
      expect(consoleWarnSpy).toHaveBeenCalled();
    });

    it('enableTrack warns on invalid userId', () => {
      const gameWebrtc = makeFullGameWebrtc();
      const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);

      surface.enableTrack(123);
      expect(consoleWarnSpy).toHaveBeenCalled();
    });
  });

  describe('disableTrack', () => {
    it('sets track.enabled = false on all video tracks for the user', () => {
      const stream = makeStream(true);
      const gameWebrtc = makeGameWebrtc(stream);
      const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);

      surface.disableTrack('user-123');

      expect(gameWebrtc.client.getMediaStreamForUser).toHaveBeenCalledWith('user-123');
      expect(stream._track.enabled).toBe(false);
    });

    it('logs [ScryingPool] warning when no video tracks found (stream has none)', () => {
      const emptyStream = { getVideoTracks: vi.fn().mockReturnValue([]) };
      const gameWebrtc = makeGameWebrtc(emptyStream);
      const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);

      surface.disableTrack('user-456');

      expect(consoleWarnSpy).toHaveBeenCalledWith(
        '[ScryingPool] disableTrack: no video tracks found for',
        'user-456',
      );
    });

    it('logs [ScryingPool] warning when getMediaStreamForUser returns null', () => {
      const gameWebrtc = makeGameWebrtc(null);
      const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);

      surface.disableTrack('user-789');

      expect(consoleWarnSpy).toHaveBeenCalledWith(
        '[ScryingPool] disableTrack: no video tracks found for',
        'user-789',
      );
    });

    it('catches errors and logs [ScryingPool] error without throwing', () => {
      const gameWebrtc = {
        client: {
          getMediaStreamForUser: vi.fn().mockImplementation(() => {
            throw new Error('AV backend unavailable');
          }),
        },
      };
      const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);

      expect(() => surface.disableTrack('user-err')).not.toThrow();
      expect(consoleErrorSpy).toHaveBeenCalledWith(
        '[ScryingPool] disableTrack failed:',
        expect.any(Error),
      );
    });
  });

  describe('enableTrack', () => {
    it('sets track.enabled = true on all video tracks for the user', () => {
      const stream = makeStream(false);
      const gameWebrtc = makeGameWebrtc(stream);
      const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);

      surface.enableTrack('user-123');

      expect(gameWebrtc.client.getMediaStreamForUser).toHaveBeenCalledWith('user-123');
      expect(stream._track.enabled).toBe(true);
    });

    it('logs [ScryingPool] warning when no video tracks found', () => {
      const emptyStream = { getVideoTracks: vi.fn().mockReturnValue([]) };
      const gameWebrtc = makeGameWebrtc(emptyStream);
      const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);

      surface.enableTrack('user-456');

      expect(consoleWarnSpy).toHaveBeenCalledWith(
        '[ScryingPool] enableTrack: no video tracks found for',
        'user-456',
      );
    });

    it('catches errors and logs [ScryingPool] error without throwing', () => {
      const gameWebrtc = {
        client: {
          getMediaStreamForUser: vi.fn().mockImplementation(() => {
            throw new Error('AV backend unavailable');
          }),
        },
      };
      const surface = FoundryAdapter.buildWebRTCSurface(gameWebrtc);

      expect(() => surface.enableTrack('user-err')).not.toThrow();
      expect(consoleErrorSpy).toHaveBeenCalledWith(
        '[ScryingPool] enableTrack failed:',
        expect.any(Error),
      );
    });
  });
});

// ─── Constructor + interface shape parity ─────────────────────────────────────

describe('FoundryAdapter constructor', () => {
  beforeEach(() => {
    vi.stubGlobal('Hooks', HOOKS_STUB);
    vi.stubGlobal('ui', UI_STUB);
    vi.clearAllMocks();
  });
  afterEach(() => {
    vi.unstubAllGlobals();
  });

  it('is side-effect-free — instantiates without accessing game.* (no game arg)', () => {
    expect(() => new FoundryAdapter()).not.toThrow();
  });

  it('accepts game as constructor arg and stores it', () => {
    const adapter = new FoundryAdapter(GAME_STUB);
    expect(adapter._game).toBe(GAME_STUB);
  });

  it('has webrtc = null by default (css-fallback path)', () => {
    const adapter = new FoundryAdapter(GAME_STUB);
    expect(adapter.webrtc).toBeNull();
  });

  it('exposes SETTINGS_NS and SETTING_WEBRTC_MODE static constants', () => {
    expect(typeof FoundryAdapter.SETTINGS_NS).toBe('string');
    expect(FoundryAdapter.SETTINGS_NS).toBe('scrying-pool');
    expect(typeof FoundryAdapter.SETTING_WEBRTC_MODE).toBe('string');
    expect(FoundryAdapter.SETTING_WEBRTC_MODE).toBe('webrtcMode');
  });

  it('exposes all 6 surfaces: settings, socket, users, scenes, notifications, hooks', () => {
    const adapter = new FoundryAdapter(GAME_STUB);
    expect(typeof adapter.settings).toBe('object');
    expect(typeof adapter.socket).toBe('object');
    expect(typeof adapter.users).toBe('object');
    expect(typeof adapter.scenes).toBe('object');
    expect(typeof adapter.notifications).toBe('object');
    expect(typeof adapter.hooks).toBe('object');
  });
});

// ─── Surface delegation ────────────────────────────────────────────────────────

describe('FoundryAdapter surface delegation', () => {
  let adapter;

  beforeEach(() => {
    vi.stubGlobal('Hooks', HOOKS_STUB);
    vi.stubGlobal('ui', UI_STUB);
    vi.clearAllMocks();
    adapter = new FoundryAdapter(GAME_STUB);
  });
  afterEach(() => {
    vi.unstubAllGlobals();
  });

  describe('settings surface', () => {
    it('settings.register delegates to game.settings.register with namespace', () => {
      adapter.settings.register('visibilityMatrix', { default: {} });
      expect(SETTINGS_STUB.register).toHaveBeenCalledWith('scrying-pool', 'visibilityMatrix', { default: {} });
    });

    it('settings.get delegates to game.settings.get with namespace', () => {
      SETTINGS_STUB.get.mockReturnValue({ userId1: 'active' });
      const result = adapter.settings.get('visibilityMatrix');
      expect(SETTINGS_STUB.get).toHaveBeenCalledWith('scrying-pool', 'visibilityMatrix');
      expect(result).toEqual({ userId1: 'active' });
    });

    it('settings.set delegates to game.settings.set with namespace', async () => {
      SETTINGS_STUB.set.mockResolvedValue({ userId1: 'hidden' });
      await adapter.settings.set('visibilityMatrix', { userId1: 'hidden' });
      expect(SETTINGS_STUB.set).toHaveBeenCalledWith('scrying-pool', 'visibilityMatrix', { userId1: 'hidden' });
    });
  });

  describe('socket surface', () => {
    it('socket.emit delegates to game.socket.emit', () => {
      adapter.socket.emit('scrying-pool.visibility.set', { opId: 'op-1' });
      expect(SOCKET_STUB.emit).toHaveBeenCalledWith('scrying-pool.visibility.set', { opId: 'op-1' });
    });

    it('socket.on delegates to game.socket.on', () => {
      const handler = vi.fn();
      adapter.socket.on('scrying-pool.visibility.set', handler);
      expect(SOCKET_STUB.on).toHaveBeenCalledWith('scrying-pool.visibility.set', handler);
    });

    it('socket.off delegates to game.socket.off', () => {
      const handler = vi.fn();
      adapter.socket.off('scrying-pool.visibility.set', handler);
      expect(SOCKET_STUB.off).toHaveBeenCalledWith('scrying-pool.visibility.set', handler);
    });
  });

  describe('users surface', () => {
    it('users.get returns user by id', () => {
      const result = adapter.users.get(GM_USER.id);
      expect(USERS_STUB.get).toHaveBeenCalledWith(GM_USER.id);
      expect(result).toBe(GM_USER);
    });

    it('users.get returns null for unknown id', () => {
      expect(adapter.users.get('unknown-id')).toBeNull();
    });

    it('users.all returns array from game.users iteration', () => {
      const result = adapter.users.all();
      expect(Array.isArray(result)).toBe(true);
      expect(result).toContain(GM_USER);
      expect(result).toContain(PLAYER_USER);
    });

    it('users.isGM(id) checks a specific user', () => {
      expect(adapter.users.isGM(GM_USER.id)).toBe(true);
      expect(adapter.users.isGM(PLAYER_USER.id)).toBe(false);
    });

    it('users.isGM() with no arg checks current user', () => {
      expect(adapter.users.isGM()).toBe(true); // USER_STUB is GM
    });

    it('users.current() returns game.user', () => {
      expect(adapter.users.current()).toEqual({ id: GM_USER.id, name: GM_USER.name, isGM: true });
    });

    describe('user flag methods', () => {
      it('users.getFlag returns flag value for valid user, scope, and key', () => {
        // First set a flag on the GM user
        GM_USER.setFlag('scrying-pool', 'testFlag', 'testValue');
        const result = adapter.users.getFlag(GM_USER.id, 'scrying-pool', 'testFlag');
        expect(result).toBe('testValue');
        expect(USERS_STUB.get).toHaveBeenCalledWith(GM_USER.id);
      });

      it('users.getFlag returns null when flag does not exist', () => {
        const result = adapter.users.getFlag(GM_USER.id, 'scrying-pool', 'nonExistentFlag');
        expect(result).toBeNull();
      });

      it('users.getFlag returns null when user does not exist', () => {
        const result = adapter.users.getFlag('unknown-user-id', 'scrying-pool', 'testFlag');
        expect(result).toBeNull();
        expect(USERS_STUB.get).toHaveBeenCalledWith('unknown-user-id');
      });

      it('users.setFlag sets flag value for valid user', async () => {
        const promise = adapter.users.setFlag(PLAYER_USER.id, 'scrying-pool', 'reactionCamEnabled', true);
        expect(promise).not.toBeNull();
        await promise;
        expect(USERS_STUB.get).toHaveBeenCalledWith(PLAYER_USER.id);
        // Verify the flag was set
        expect(PLAYER_USER.getFlag('scrying-pool', 'reactionCamEnabled')).toBe(true);
      });

      it('users.setFlag returns null when user does not exist', () => {
        const promise = adapter.users.setFlag('unknown-user-id', 'scrying-pool', 'testFlag', true);
        expect(promise).toBeNull();
        expect(USERS_STUB.get).toHaveBeenCalledWith('unknown-user-id');
      });

      it('users.getFlagModule returns module-scoped flag', () => {
        GM_USER.setFlag('scrying-pool', 'reactionCamEnabled', false);
        const result = adapter.users.getFlagModule(GM_USER.id, 'reactionCamEnabled');
        expect(result).toBe(false);
      });

      it('users.getFlagModule returns null when flag does not exist', () => {
        const result = adapter.users.getFlagModule(GM_USER.id, 'nonExistentFlag');
        expect(result).toBeNull();
      });

      it('users.setFlagModule sets module-scoped flag', async () => {
        const promise = adapter.users.setFlagModule(PLAYER_USER.id, 'reactionCamEnabled', true);
        expect(promise).not.toBeNull();
        await promise;
        expect(PLAYER_USER.getFlag('scrying-pool', 'reactionCamEnabled')).toBe(true);
      });
    });
  });

  describe('scenes surface', () => {
    it('scenes.current() returns game.scenes.active', () => {
      expect(adapter.scenes.current()).toBe(ACTIVE_SCENE);
    });

    it('scenes.get(id) delegates to game.scenes.get', () => {
      const result = adapter.scenes.get(ACTIVE_SCENE.id);
      expect(SCENES_STUB.get).toHaveBeenCalledWith(ACTIVE_SCENE.id);
      expect(result).toBe(ACTIVE_SCENE);
    });

    it('scenes.get returns null for unknown id', () => {
      expect(adapter.scenes.get('no-such-scene')).toBeNull();
    });
  });

  describe('notifications surface', () => {
    it('notifications.info delegates to ui.notifications.info', () => {
      adapter.notifications.info('Test info');
      expect(UI_STUB.notifications.info).toHaveBeenCalledWith('Test info');
    });

    it('notifications.warn delegates to ui.notifications.warn', () => {
      adapter.notifications.warn('Test warn');
      expect(UI_STUB.notifications.warn).toHaveBeenCalledWith('Test warn');
    });

    it('notifications.error delegates to ui.notifications.error', () => {
      adapter.notifications.error('Test error');
      expect(UI_STUB.notifications.error).toHaveBeenCalledWith('Test error');
    });
  });

  describe('hooks surface', () => {
    it('hooks.on delegates to Hooks.on', () => {
      const handler = vi.fn();
      adapter.hooks.on('ready', handler);
      expect(HOOKS_STUB.on).toHaveBeenCalledWith('ready', handler);
    });

    it('hooks.once delegates to Hooks.once', () => {
      const handler = vi.fn();
      adapter.hooks.once('ready', handler);
      expect(HOOKS_STUB.once).toHaveBeenCalledWith('ready', handler);
    });

    it('hooks.off delegates to Hooks.off', () => {
      const handler = vi.fn();
      adapter.hooks.off('ready', handler);
      expect(HOOKS_STUB.off).toHaveBeenCalledWith('ready', handler);
    });
  });
});
