// @ts-nocheck
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { TokenVideoOverlay } from '../../../../src/ui/shared/TokenVideoOverlay.js';
import { createFoundryAdapterMock } from '../../../helpers/foundryAdapterMock.js';

describe('TokenVideoOverlay', () => {
  let adapter;
  let overlay;
  let mockToken;
  let rafCallbacks;

  beforeEach(() => {
    rafCallbacks = [];
    let rafIdCounter = 0;
    vi.stubGlobal('PIXI', {
      Texture: {
        from: vi.fn(() => {
          const tex = { baseTexture: { update: vi.fn() } };
          return tex;
        }),
      },
    });
    vi.stubGlobal('requestAnimationFrame', (cb) => {
      rafCallbacks.push(cb);
      return ++rafIdCounter;
    });
    vi.stubGlobal('cancelAnimationFrame', vi.fn());
    vi.stubGlobal('Hooks', { on: vi.fn(), once: vi.fn() });

    globalThis.canvas = {
      scene: { id: 'scene-1' },
      tokens: { placeables: [], get: vi.fn() },
    };
    globalThis.CONST = { DOCUMENT_PERMISSION_LEVELS: { OWNER: 3 } };
    globalThis.foundry = {
      canvas: { placeables: { Token: class {} } },
    };

    HTMLVideoElement.prototype.play = vi.fn(() => Promise.resolve());
    // happy-dom rejects non-MediaStream srcObject values
    Object.defineProperty(HTMLVideoElement.prototype, 'srcObject', {
      writable: true,
      value: null,
    });

    const mockStream = {
      getVideoTracks: vi.fn(() => [{ enabled: true }]),
    };

    adapter = createFoundryAdapterMock({
      webrtc: {
        getMediaStreamForUser: vi.fn(() => mockStream),
      },
      settings: {
        get: vi.fn((key) => {
          if (key === 'userActorMapping') return {};
          if (key === 'showVideoOnTokens') return true;
          return null;
        }),
      },
      users: {
        all: vi.fn(() => [{ id: 'user-1', isGM: false, active: true }]),
        get: vi.fn(() => ({ id: 'user-1', isGM: false, active: true })),
      },
    });

    mockToken = {
      id: 'token-1',
      document: {
        actor: {
          id: 'actor-1',
          testUserPermission: vi.fn(() => true),
        },
      },
      mesh: { width: 100, height: 100, texture: 'original-tex' },
    };

    overlay = new TokenVideoOverlay(adapter);
  });

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

  const tickRAF = () => {
    const cb = rafCallbacks.shift();
    if (cb) cb();
  };

  describe('constructor', () => {
    it('stores adapter reference without side effects', () => {
      expect(overlay._adapter).toBe(adapter);
      expect(overlay._overlays).toBeInstanceOf(Map);
      expect(overlay._overlays.size).toBe(0);
      expect(overlay._pending).toBeInstanceOf(Set);
      expect(overlay._pending.size).toBe(0);
      expect(overlay._origRefresh).toBeNull();
      expect(overlay._enabled).toBe(false);
    });
  });

  describe('_attach() — canvas 2D fallback', () => {
    // happy-dom getContext('2d') returns null — fallback path

    beforeEach(() => {
      overlay._enabled = true;
    });

    it('falls back to direct video texture when canvas 2D unavailable', () => {
      overlay._attach(mockToken);
      tickRAF();

      expect(overlay._overlays.size).toBe(1);
      const data = overlay._overlays.get('scene-1.token-1');

      expect(data.canvas).toBeUndefined();
      expect(data.ctx).toBeUndefined();
      expect(data.pixiTexture).toBeUndefined();
      expect(data.origTexture).toBe('original-tex');
      expect(data.userId).toBe('user-1');
      expect(data.videoEl).toBeInstanceOf(HTMLVideoElement);
      expect(PIXI.Texture.from).toHaveBeenCalled();
    });

    it('does not create overlay when token has no mesh', () => {
      const badToken = { id: 'bad', document: { actor: { id: 'a1' } } };
      overlay._attach(badToken);
      tickRAF();

      expect(overlay._overlays.size).toBe(0);
    });

    it('clears pending when attach completes', () => {
      overlay._attach(mockToken);
      expect(overlay._pending.has('scene-1.token-1')).toBe(true);
      tickRAF();
      expect(overlay._pending.has('scene-1.token-1')).toBe(false);
    });
  });

  describe('_attach() — circular canvas path', () => {
    let mockCtx;

    beforeEach(() => {
      overlay._enabled = true;
      mockCtx = {
        clearRect: vi.fn(),
        save: vi.fn(),
        beginPath: vi.fn(),
        arc: vi.fn(),
        clip: vi.fn(),
        drawImage: vi.fn(),
        restore: vi.fn(),
      };
      vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(mockCtx);
    });

    it('creates canvas sized to mesh dimensions', () => {
      overlay._attach(mockToken);
      tickRAF();

      const data = overlay._overlays.get('scene-1.token-1');
      expect(data.canvas).toBeInstanceOf(HTMLCanvasElement);
      expect(data.canvas.width).toBe(100);
      expect(data.canvas.height).toBe(100);
      expect(data.ctx).toBe(mockCtx);
      expect(data.rafId).toBeTypeOf('number');
      expect(data.pixiTexture).toBeDefined();
    });

    it('sets mesh texture to canvas-based PIXI texture', () => {
      overlay._attach(mockToken);
      tickRAF();

      const data = overlay._overlays.get('scene-1.token-1');
      expect(mockToken.mesh.texture).toBe(data.pixiTexture);
      expect(mockToken.mesh.texture).not.toBe('original-tex');
    });

    it('runs render loop drawing circular clip each frame', () => {
      overlay._attach(mockToken);
      tickRAF();

      const data = overlay._overlays.get('scene-1.token-1');
      const firstRafId = data.rafId;

      tickRAF();

      expect(mockCtx.clearRect).toHaveBeenCalledWith(0, 0, 100, 100);
      expect(mockCtx.save).toHaveBeenCalled();
      expect(mockCtx.beginPath).toHaveBeenCalled();
      expect(mockCtx.arc).toHaveBeenCalledWith(50, 50, 50, 0, Math.PI * 2);
      expect(mockCtx.clip).toHaveBeenCalled();
      expect(mockCtx.drawImage).toHaveBeenCalled();
      expect(mockCtx.restore).toHaveBeenCalled();

      expect(data.pixiTexture.baseTexture.update).toHaveBeenCalled();

      expect(data.rafId).not.toBe(firstRafId);
    });

    it('uses min dimension radius for non-square tokens', () => {
      mockToken.mesh.width = 200;
      mockToken.mesh.height = 100;

      overlay._attach(mockToken);
      tickRAF();
      tickRAF();

      expect(mockCtx.arc).toHaveBeenCalledWith(100, 50, 50, 0, Math.PI * 2);
    });

    it('guards against zero-dimension mesh', () => {
      mockToken.mesh.width = 0;
      mockToken.mesh.height = 0;

      overlay._attach(mockToken);
      tickRAF();

      const data = overlay._overlays.get('scene-1.token-1');
      expect(data.canvas.width).toBe(1);
      expect(data.canvas.height).toBe(1);
    });

    it('stops render loop when overlay is removed from map', () => {
      overlay._attach(mockToken);
      tickRAF();

      overlay._overlays.delete('scene-1.token-1');
      rafCallbacks.length = 0;

      tickRAF();
      expect(mockCtx.clearRect).not.toHaveBeenCalled();
    });

    it('stops render loop when disabled', () => {
      overlay._attach(mockToken);
      tickRAF();

      overlay._enabled = false;
      rafCallbacks.length = 0;

      tickRAF();
      expect(mockCtx.clearRect).not.toHaveBeenCalled();
    });
  });

  describe('_detachByKey()', () => {
    beforeEach(() => {
      overlay._overlays.set('scene-1.token-1', {
        videoEl: document.createElement('video'),
        origTexture: 'original-tex',
        userId: 'user-1',
        canvas: document.createElement('canvas'),
        pixiTexture: { baseTexture: { update: vi.fn() } },
        rafId: 42,
      });
      globalThis.canvas.tokens.get = vi.fn(() => mockToken);
    });

    it('cancels rAF loop', () => {
      overlay._detachByKey('scene-1.token-1');
      expect(cancelAnimationFrame).toHaveBeenCalledWith(42);
    });

    it('restores original texture on mesh', () => {
      mockToken.mesh.texture = 'overwritten-by-something';
      overlay._detachByKey('scene-1.token-1');
      expect(mockToken.mesh.texture).toBe('original-tex');
    });

    it('removes video element from DOM', () => {
      const data = overlay._overlays.get('scene-1.token-1');
      document.body.appendChild(data.videoEl);

      overlay._detachByKey('scene-1.token-1');
      expect(document.body.contains(data.videoEl)).toBe(false);
    });

    it('removes overlay from map', () => {
      overlay._detachByKey('scene-1.token-1');
      expect(overlay._overlays.has('scene-1.token-1')).toBe(false);
    });

    it('is no-op for unknown key', () => {
      expect(() => overlay._detachByKey('unknown')).not.toThrow();
    });

    it('is idempotent', () => {
      overlay._detachByKey('scene-1.token-1');
      overlay._detachByKey('scene-1.token-1');
      expect(cancelAnimationFrame).toHaveBeenCalledTimes(1);
    });
  });

  describe('_onTokenRefreshed()', () => {
    let mockPixiTex;

    beforeEach(() => {
      overlay._enabled = true;
      mockPixiTex = { baseTexture: { update: vi.fn() } };
      const c = document.createElement('canvas');
      c.width = 100;
      c.height = 100;
      overlay._overlays.set('scene-1.token-1', {
        videoEl: document.createElement('video'),
        origTexture: 'original-tex',
        userId: 'user-1',
        canvas: c,
        ctx: { clearRect: vi.fn() },
        pixiTexture: mockPixiTex,
        rafId: 42,
      });
      globalThis.canvas.tokens.get = vi.fn(() => mockToken);
    });

    it('re-applies pixiTexture when mesh texture was lost', () => {
      mockToken.mesh.texture = 'wrong-texture';
      overlay._onTokenRefreshed(mockToken);
      expect(mockToken.mesh.texture).toBe(mockPixiTex);
    });

    it('detects mesh resize and re-attaches', () => {
      vi.spyOn(overlay, '_detachByKey');
      vi.spyOn(overlay, '_attach');

      mockToken.mesh.width = 200;
      mockToken.mesh.height = 200;
      overlay._onTokenRefreshed(mockToken);

      expect(overlay._detachByKey).toHaveBeenCalledWith('scene-1.token-1');
      expect(overlay._attach).toHaveBeenCalledWith(mockToken);
    });

    it('does nothing when mesh size and texture match', () => {
      vi.spyOn(overlay, '_detachByKey');
      vi.spyOn(overlay, '_attach');

      mockToken.mesh.texture = mockPixiTex;
      overlay._onTokenRefreshed(mockToken);

      expect(overlay._detachByKey).not.toHaveBeenCalled();
      expect(overlay._attach).not.toHaveBeenCalled();
    });

    it('uses fallback video path for non-canvas overlays', () => {
      overlay._overlays.set('scene-1.token-2', {
        videoEl: document.createElement('video'),
        origTexture: 'original-tex-2',
        userId: 'user-2',
      });

      const token2 = {
        id: 'token-2',
        document: mockToken.document,
        mesh: { width: 100, height: 100, texture: 'wrong' },
      };

      overlay._onTokenRefreshed(token2);
      expect(PIXI.Texture.from).toHaveBeenCalled();
    });

    it('attaches when overlay missing and not pending', () => {
      overlay._overlays.clear();
      vi.spyOn(overlay, '_attach');

      overlay._onTokenRefreshed(mockToken);
      expect(overlay._attach).toHaveBeenCalledWith(mockToken);
    });

    it('skips attach when token is pending', () => {
      overlay._overlays.clear();
      overlay._pending.add('scene-1.token-1');

      vi.spyOn(overlay, '_attach');
      overlay._onTokenRefreshed(mockToken);
      expect(overlay._attach).not.toHaveBeenCalled();
    });
  });

  describe('_onUpdateToken()', () => {
    let mockPixiTex;

    beforeEach(() => {
      overlay._enabled = true;
      mockPixiTex = { baseTexture: { update: vi.fn() } };
      overlay._overlays.set('scene-1.token-1', {
        videoEl: document.createElement('video'),
        origTexture: 'original-tex',
        userId: 'user-1',
        canvas: document.createElement('canvas'),
        pixiTexture: mockPixiTex,
        rafId: 42,
      });
      globalThis.canvas.tokens.get = vi.fn(() => mockToken);
    });

    it('re-applies pixiTexture when mesh lost it', () => {
      mockToken.mesh.texture = 'wrong-texture';
      overlay._onUpdateToken(mockToken.document);
      expect(mockToken.mesh.texture).toBe(mockPixiTex);
    });

    it('detaches when token loses user', () => {
      adapter.users.all = vi.fn(() => []);
      overlay._onUpdateToken(mockToken.document);
      expect(overlay._overlays.has('scene-1.token-1')).toBe(false);
    });

    it('detaches old and re-attaches when user changes', () => {
      overlay._overlays.set('scene-1.token-1', {
        videoEl: document.createElement('video'),
        origTexture: 'original-tex',
        userId: 'old-user',
      });

      vi.spyOn(overlay, '_detachByKey');
      vi.spyOn(overlay, '_attach');

      overlay._onUpdateToken(mockToken.document);
      expect(overlay._detachByKey).toHaveBeenCalledWith('scene-1.token-1');
      expect(overlay._attach).toHaveBeenCalledWith(mockToken);
    });

    it('is no-op when token not on canvas', () => {
      globalThis.canvas.tokens.get = vi.fn(() => null);
      expect(() => overlay._onUpdateToken(mockToken.document)).not.toThrow();
    });
  });

  describe('enable/disable', () => {
    it('enable sets _enabled and calls syncAll', () => {
      vi.spyOn(overlay, 'syncAll');
      overlay.enable();
      expect(overlay._enabled).toBe(true);
      expect(overlay.syncAll).toHaveBeenCalled();
    });

    it('disable sets _enabled and calls _cleanupAll', () => {
      vi.spyOn(overlay, '_cleanupAll');
      overlay.enable();
      overlay.disable();
      expect(overlay._enabled).toBe(false);
      expect(overlay._cleanupAll).toHaveBeenCalled();
    });
  });

  describe('_cleanupAll()', () => {
    it('detaches all overlays', () => {
      vi.spyOn(overlay, '_detachByKey');
      overlay._overlays.set('scene-1.token-1', { origTexture: 't1' });
      overlay._overlays.set('scene-1.token-2', { origTexture: 't2' });
      overlay._cleanupAll();
      expect(overlay._detachByKey).toHaveBeenCalledWith('scene-1.token-1');
      expect(overlay._detachByKey).toHaveBeenCalledWith('scene-1.token-2');
      expect(overlay._detachByKey).toHaveBeenCalledTimes(2);
    });
  });

  describe('syncAll()', () => {
    beforeEach(() => {
      overlay._enabled = true;
    });

    it('attaches to all canvas tokens', () => {
      vi.spyOn(overlay, '_attach');
      canvas.tokens.placeables = [mockToken];
      overlay.syncAll();
      expect(overlay._attach).toHaveBeenCalledWith(mockToken);
    });

    it('detaches overlays for tokens no longer on canvas', () => {
      overlay._overlays.set('scene-1.token-1', {});
      overlay._overlays.set('scene-1.token-2', {});
      overlay._overlays.set('scene-2.token-3', {});
      canvas.tokens.placeables = [mockToken];

      vi.spyOn(overlay, '_detachByKey');
      overlay.syncAll();
      expect(overlay._detachByKey).toHaveBeenCalledWith('scene-1.token-2');
      expect(overlay._detachByKey).toHaveBeenCalledWith('scene-2.token-3');
      expect(overlay._detachByKey).not.toHaveBeenCalledWith('scene-1.token-1');
    });
  });
});
