// @ts-nocheck
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';

// Stub foundry globals before importing ScryingPoolStrip.
// Do NOT include foundry.applications.api.ApplicationV2 so the fallback class is used.
beforeEach(() => {
  vi.stubGlobal('foundry', {
    utils: {
      mergeObject: (base, override) => Object.assign({}, base, override),
    },
  });
  vi.stubGlobal('game', {
    user: {
      setFlag: vi.fn(),
      getFlag: vi.fn(() => null),
    },
  });
  vi.stubGlobal('document', {
    createElement: vi.fn(tag => ({ tag, srcObject: null, autoplay: false, playsInline: false, muted: false, className: '', addEventListener: vi.fn(), remove: vi.fn() })),
    querySelectorAll: vi.fn(() => []),
  });
});

afterEach(() => {
  vi.unstubAllGlobals();
  vi.clearAllMocks();
});

import {
  LABELS,
  resolveTargetState,
  buildParticipantList,
  ScryingPoolStrip,
} from '../../../../src/ui/gm/ScryingPoolStrip.js';

describe('LABELS', () => {
  it('has HIDE_FROM_TABLE equal to exact canonical string', () => {
    expect(LABELS.HIDE_FROM_TABLE).toBe('Hide from table');
  });

  it('has SHOW_TO_TABLE equal to exact canonical string', () => {
    expect(LABELS.SHOW_TO_TABLE).toBe('Show to table');
  });

  it('has FIRST_TOOLTIP set', () => {
    expect(LABELS.FIRST_TOOLTIP).toBe('Hide this participant from other players.');
  });

  it('is frozen (immutable)', () => {
    expect(Object.isFrozen(LABELS)).toBe(true);
  });
});

describe('resolveTargetState()', () => {
  it('returns active when current state is hidden', () => {
    expect(resolveTargetState('hidden')).toBe('active');
  });

  it('returns hidden when current state is active', () => {
    expect(resolveTargetState('active')).toBe('hidden');
  });

  it('returns hidden when current state is self-muted', () => {
    expect(resolveTargetState('self-muted')).toBe('hidden');
  });

  it('returns hidden when current state is cam-lost', () => {
    expect(resolveTargetState('cam-lost')).toBe('hidden');
  });

  it('returns hidden when current state is never-connected', () => {
    expect(resolveTargetState('never-connected')).toBe('hidden');
  });
});

describe('buildParticipantList()', () => {
  let stateStore;
  let controller;
  let adapter;

  beforeEach(() => {
    stateStore = {
      getState: vi.fn(userId => userId === 'user-1' ? 'active' : 'hidden'),
    };
    controller = {
      hasPendingOp: vi.fn(() => false),
    };
    adapter = {
      users: {
        get: vi.fn(userId => ({
          id: userId,
          name: `User ${userId}`,
          avatar: `avatars/${userId}.png`,
        })),
        current: vi.fn(() => ({ id: 'user-1' })),
      },
    };
  });

  it('returns correct shape for each participant', () => {
    const list = buildParticipantList(['user-1', 'user-2'], stateStore, controller, adapter);
    expect(list).toHaveLength(2);
    expect(list[0]).toMatchObject({
      userId: 'user-1',
      name: 'User user-1',
      avatarSrc: 'avatars/user-1.png',
      state: 'active',
      stateLabel: 'Active',
      hasPendingOp: false,
      isCurrentUser: true,
    });
  });

  it('returns isEmpty-compatible empty array for no userIds', () => {
    const list = buildParticipantList([], stateStore, controller, adapter);
    expect(list).toHaveLength(0);
  });

  it('sets hasPendingOp to true when controller reports pending', () => {
    controller.hasPendingOp.mockReturnValue(true);
    const list = buildParticipantList(['user-1'], stateStore, controller, adapter);
    expect(list[0].hasPendingOp).toBe(true);
  });

  it('uses mystery-man.svg fallback when avatar is null', () => {
    adapter.users.get.mockReturnValue({ id: 'user-1', name: 'Alice', avatar: null });
    const list = buildParticipantList(['user-1'], stateStore, controller, adapter);
    expect(list[0].avatarSrc).toBe('icons/svg/mystery-man.svg');
  });

  it('marks only the current user as isCurrentUser', () => {
    const list = buildParticipantList(['user-1', 'user-2'], stateStore, controller, adapter);
    expect(list[0].isCurrentUser).toBe(true);
    expect(list[1].isCurrentUser).toBe(false);
  });

  it('correctly maps hidden state label', () => {
    const list = buildParticipantList(['user-2'], stateStore, controller, adapter);
    expect(list[0].stateLabel).toBe('Hidden');
  });

  it('includes hasStreamAccess flag when passed as true (Story 5.1)', () => {
    const list = buildParticipantList(['user-1'], stateStore, controller, adapter, true);
    expect(list[0].hasStreamAccess).toBe(true);
  });

  it('includes hasStreamAccess flag when passed as false (Story 5.1)', () => {
    const list = buildParticipantList(['user-1'], stateStore, controller, adapter, false);
    expect(list[0].hasStreamAccess).toBe(false);
  });

  it('defaults hasStreamAccess to false when not provided (Story 5.1)', () => {
    const list = buildParticipantList(['user-1'], stateStore, controller, adapter);
    expect(list[0].hasStreamAccess).toBe(false);
  });
});

describe('ScryingPoolStrip', () => {
  let stateStore;
  let controller;
  let avTileAdapter;
  let adapter;
  let strip;

  beforeEach(() => {
    stateStore = { getState: vi.fn(() => 'active') };
    controller = { action: vi.fn(), getRevision: vi.fn(() => 0), hasPendingOp: vi.fn(() => false) };
    avTileAdapter = { mount: vi.fn(), unmount: vi.fn(), setStateClass: vi.fn(), disconnect: vi.fn() };
    adapter = {
      users: {
        get: vi.fn(() => ({ id: 'u1', name: 'Alice', avatar: 'av.png' })),
        all: vi.fn(() => [{ id: 'u1' }]),
        current: vi.fn(() => ({ id: 'u1' })),
      },
    };
    strip = new ScryingPoolStrip(stateStore, controller, avTileAdapter, adapter);
  });

  describe('DEFAULT_OPTIONS', () => {
    it('has correct id', () => {
      expect(ScryingPoolStrip.DEFAULT_OPTIONS.id).toBe('scrying-pool-strip');
    });

    it('has correct template path', () => {
      expect(ScryingPoolStrip.PARTS.strip.template).toContain('roster-strip.hbs');
    });

    it('is not resizable', () => {
      expect(ScryingPoolStrip.DEFAULT_OPTIONS.window?.resizable).toBe(false);
    });

    it('has title set', () => {
      expect(ScryingPoolStrip.DEFAULT_OPTIONS.window?.title).toBe('Scrying Pool');
    });
  });

  describe('_prepareContext()', () => {
    it('returns participants array', async () => {
      const data = await strip._prepareContext({});
      expect(Array.isArray(data.participants)).toBe(true);
    });

    it('returns isExpanded property', async () => {
      const data = await strip._prepareContext({});
      expect(typeof data.isExpanded).toBe('boolean');
    });

    it('returns isEmpty true when no participants', async () => {
      adapter.users.all.mockReturnValue([]);
      const data = await strip._prepareContext({});
      expect(data.isEmpty).toBe(true);
    });

    it('returns isEmpty false when participants exist', async () => {
      const data = await strip._prepareContext({});
      expect(data.isEmpty).toBe(false);
    });

    it('returns hasStreamAccess true when webrtc.getMediaStreamForUser is available (Story 5.1)', async () => {
      adapter.webrtc = {
        getMediaStreamForUser: vi.fn(),
      };
      const data = await strip._prepareContext({});
      expect(data.hasStreamAccess).toBe(true);
    });

    it('returns hasStreamAccess false when webrtc is null (Story 5.1)', async () => {
      adapter.webrtc = null;
      const data = await strip._prepareContext({});
      expect(data.hasStreamAccess).toBe(false);
    });

    it('returns hasStreamAccess false when webrtc has no getMediaStreamForUser (Story 5.1)', async () => {
      adapter.webrtc = {};
      const data = await strip._prepareContext({});
      expect(data.hasStreamAccess).toBe(false);
    });

    it('includes current user when showGMSelfFeed is true', async () => {
      adapter.settings = { get: vi.fn(() => true) };
      adapter.users.all.mockReturnValue([{ id: 'u1' }, { id: 'u2' }]);
      const data = await strip._prepareContext({});
      expect(data.participants.map(p => p.userId)).toContain('u1');
      expect(data.participants.map(p => p.userId)).toContain('u2');
    });

    it('excludes current user when showGMSelfFeed is false', async () => {
      adapter.settings = { get: vi.fn(() => false) };
      adapter.users.all.mockReturnValue([{ id: 'u1' }, { id: 'u2' }]);
      const data = await strip._prepareContext({});
      // u1 is the current user (mocked in beforeEach), should be excluded
      expect(data.participants.map(p => p.userId)).not.toContain('u1');
      expect(data.participants.map(p => p.userId)).toContain('u2');
    });

    it('includes all users when settings is unavailable (defaults to true)', async () => {
      // no adapter.settings — fallback to true
      adapter.users.all.mockReturnValue([{ id: 'u1' }, { id: 'u2' }]);
      const data = await strip._prepareContext({});
      expect(data.participants.length).toBe(2);
    });

    it('excludes hidden participants from the strip to keep it compact', async () => {
      stateStore.getState.mockImplementation(id => id === 'u1' ? 'hidden' : 'active');
      adapter.users.all.mockReturnValue([{ id: 'u1' }, { id: 'u2' }]);
      const data = await strip._prepareContext({});
      expect(data.participants.map(p => p.userId)).not.toContain('u1');
      expect(data.participants.map(p => p.userId)).toContain('u2');
    });

    it('returns isEmpty true when all participants are hidden', async () => {
      stateStore.getState.mockReturnValue('hidden');
      adapter.users.all.mockReturnValue([{ id: 'u1' }, { id: 'u2' }]);
      const data = await strip._prepareContext({});
      expect(data.isEmpty).toBe(true);
      expect(data.participants.length).toBe(0);
    });

    it('returns dockLayout from settings (defaults to vertical-sm when setting is not a string)', async () => {
      // dockLayout non-string → fallback to vertical-sm; no sizeOverride → canonical size 'sm'
      adapter.settings = { get: vi.fn(key => key === 'dockLayoutExpanded' ? '' : true) };
      const data = await strip._prepareContext({});
      expect(data.dockLayout).toBe('vertical-sm');
    });

    it('returns dockLayout from settings when setting is a valid string', async () => {
      // No size override → canonical size from world setting ('md') is used
      adapter.settings = { get: vi.fn(key => key === 'dockLayout' ? 'horizontal-md' : '') };
      const data = await strip._prepareContext({});
      expect(data.dockLayout).toBe('horizontal-md');
      expect(data.isExpanded).toBe(false);
      expect(data.showName).toBe(true);
    });

    it('client size override forces sm even when world layout is md', async () => {
      adapter.settings = { get: vi.fn(key => key === 'dockLayout' ? 'horizontal-md' : key === 'dockLayoutExpanded' ? 'sm' : '') };
      const data = await strip._prepareContext({});
      expect(data.dockLayout).toBe('horizontal-sm');
      expect(data.showName).toBe(false);
    });

    it('client size override forces md even when world layout is sm', async () => {
      adapter.settings = { get: vi.fn(key => key === 'dockLayout' ? 'vertical-sm' : key === 'dockLayoutExpanded' ? 'md' : '') };
      const data = await strip._prepareContext({});
      expect(data.dockLayout).toBe('vertical-md');
      expect(data.isExpanded).toBe(true);
    });

    it('sets isExpanded=true and showName=true only for vertical-md', async () => {
      adapter.settings = { get: vi.fn(key => key === 'dockLayout' ? 'vertical-md' : '') };
      const data = await strip._prepareContext({});
      expect(data.isExpanded).toBe(true);
      expect(data.showName).toBe(true);
    });
  });

  describe('_computeStripWidth()', () => {
    it('returns tileWidth + 11 for vertical-sm', () => {
      expect(strip._computeStripWidth('vertical-sm', 3)).toBe(83 + 8 + 3);
    });

    it('returns tileWidth + 11 for vertical-md', () => {
      expect(strip._computeStripWidth('vertical-md', 3)).toBe(150 + 8 + 3);
    });

    it('returns 85 for unknown layout', () => {
      expect(strip._computeStripWidth('unknown', 3)).toBe(85);
    });

    it('scales horizontal-sm width with participant count (max 4 cols)', () => {
      const w2 = strip._computeStripWidth('horizontal-sm', 2);
      const w4 = strip._computeStripWidth('horizontal-sm', 4);
      const w6 = strip._computeStripWidth('horizontal-sm', 6);
      expect(w4).toBeGreaterThan(w2);
      expect(w6).toBe(w4); // capped at 4 cols
    });

    it('horizontal-md tiles are wider than horizontal-sm', () => {
      const wSm = strip._computeStripWidth('horizontal-sm', 4);
      const wMd = strip._computeStripWidth('horizontal-md', 4);
      expect(wMd).toBeGreaterThan(wSm);
    });
  });

  describe('_computeStripHeight()', () => {
    const CHROME = 16 + 29; // grip + toolbar (29 = 28px content + 1px border-bottom)
    const BORDER_H = 2, GAP = 4, TILE_PAD = 8;

    it('returns auto for vertical layouts', () => {
      expect(strip._computeStripHeight('vertical-sm', 3)).toBe('auto');
      expect(strip._computeStripHeight('vertical-md', 3)).toBe('auto');
      expect(strip._computeStripHeight('unknown', 3)).toBe('auto');
    });

    it('horizontal-sm: 1 row for ≤4 participants', () => {
      // 4 tiles, 1 row: CHROME + 83 + 8pad + 2border = 138
      expect(strip._computeStripHeight('horizontal-sm', 4)).toBe(CHROME + 83 + TILE_PAD + BORDER_H);
    });

    it('horizontal-sm: 2 rows for 5+ participants', () => {
      // 5 tiles → cols=4, rows=2: CHROME + 2*83 + 1*4 + 8 + 2 = 225
      expect(strip._computeStripHeight('horizontal-sm', 5)).toBe(CHROME + 2 * 83 + GAP + TILE_PAD + BORDER_H);
    });

    it('horizontal-md tiles produce taller rows than horizontal-sm', () => {
      const hSm = strip._computeStripHeight('horizontal-sm', 4);
      const hMd = strip._computeStripHeight('horizontal-md', 4);
      expect(hMd).toBeGreaterThan(hSm);
    });

    it('mosaic-sm: NxN grid', () => {
      // n=4 → cols=2, rows=2: CHROME + 2*83 + 1*4 + 8 + 2 = 225
      expect(strip._computeStripHeight('mosaic-sm', 4)).toBe(CHROME + 2 * 83 + GAP + TILE_PAD + BORDER_H);
    });

    it('mosaic height grows with participant count', () => {
      const h4 = strip._computeStripHeight('mosaic-sm', 4);
      const h9 = strip._computeStripHeight('mosaic-sm', 9);
      expect(h9).toBeGreaterThan(h4);
    });
  });

  describe('_attachVideoStream() (Story 5.1)', () => {
    let mockVideoContainer;
    let consoleWarnSpy;

    beforeEach(() => {
      consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
      mockVideoContainer = {
        querySelector: vi.fn(),
        appendChild: vi.fn(),
      };
      adapter.webrtc = {
        getMediaStreamForUser: vi.fn().mockReturnValue(new MediaStream()),
      };
      vi.spyOn(document, 'createElement').mockReturnValue({
        srcObject: null,
        autoplay: false,
        playsInline: false,
        muted: false,
        className: '',
        addEventListener: vi.fn(),
      });
    });

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

    it('creates video element with stream (Story 5.1)', () => {
      const participantItem = {
        querySelector: vi.fn().mockReturnValue(mockVideoContainer),
      };

      // Call the method directly
      strip._attachVideoStream('user-1', participantItem);

      // Check that getMediaStreamForUser was called
      expect(adapter.webrtc.getMediaStreamForUser).toHaveBeenCalledWith('user-1');
      expect(consoleWarnSpy).not.toHaveBeenCalled();
    });

    it('returns early and does not warn when webrtc is not available (Story 5.1)', () => {
      adapter.webrtc = null;
      const participantItem = { querySelector: vi.fn() };

      strip._attachVideoStream('user-1', participantItem);

      // Should return early without warning since webrtc check happens first
      expect(consoleWarnSpy).not.toHaveBeenCalled();
      expect(participantItem.querySelector).not.toHaveBeenCalled();
    });

    it('warns when no video container found (Story 5.1)', () => {
      const participantItem = {
        querySelector: vi.fn().mockReturnValue(null),
      };

      strip._attachVideoStream('user-1', participantItem);

      expect(consoleWarnSpy).toHaveBeenCalledWith(
        '[ScryingPool] No video container found for user:',
        'user-1'
      );
    });
  });

  describe('_cleanupVideoStreams() (Story 5.1)', () => {
    let consoleWarnSpy;

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

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

    it('handles undefined document gracefully', () => {
      const originalDocument = global.document;
      vi.stubGlobal('document', undefined);

      expect(() => strip._cleanupVideoStreams()).not.toThrow();

      vi.stubGlobal('document', originalDocument);
    });

    it('removes video elements without stopping tracks', () => {
      const mockStream = new MediaStream();
      const mockVideoElement = {
        srcObject: mockStream,
        remove: vi.fn(),
      };

      strip.element = {
        querySelectorAll: vi.fn().mockReturnValue([mockVideoElement]),
      };

      strip._cleanupVideoStreams();

      // Tracks are NOT stopped — they belong to FoundryVTT's WebRTC system
      expect(mockVideoElement.srcObject).toBe(null);
      expect(mockVideoElement.remove).toHaveBeenCalled();
    });

    it('handles video elements without streams gracefully', () => {
      const mockVideoElement = {
        srcObject: null,
        remove: vi.fn(),
      };

      strip.element = {
        querySelectorAll: vi.fn().mockReturnValue([mockVideoElement]),
      };

      expect(() => strip._cleanupVideoStreams()).not.toThrow();
      expect(mockVideoElement.remove).toHaveBeenCalled();
    });

    it('handles empty video element list', () => {
      strip.element = {
        querySelectorAll: vi.fn().mockReturnValue([]),
      };

      expect(() => strip._cleanupVideoStreams()).not.toThrow();
    });
  });
});
