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

/** @returns {StateStore} */
function makeStateStore() {
  const settingsMock = {
    get: vi.fn().mockReturnValue({ _version: 1, matrix: {} }),
    set: vi.fn().mockResolvedValue(undefined),
    register: vi.fn(),
  };
  return new StateStore(settingsMock);
}

describe('VisibilityManager', () => {
  let adapter;
  let stateStore;
  let manager;
  let hooksStub;

  beforeEach(() => {
    hooksStub = { callAll: vi.fn(), on: vi.fn(), once: vi.fn(), off: vi.fn() };
    vi.stubGlobal('Hooks', hooksStub);

    adapter = createFoundryAdapterMock({ hooks: hooksStub });
    stateStore = makeStateStore();
    manager = new VisibilityManager(stateStore, adapter);
  });

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

  // ── AC-1 (construction side-effect free) ─────────────────────────────────

  describe('constructor (side-effect free)', () => {
    it('does NOT register Hooks.on listener in constructor', () => {
      expect(hooksStub.on).not.toHaveBeenCalled();
    });
  });

  // ── init() ────────────────────────────────────────────────────────────────

  describe('init()', () => {
    it('registers Hooks.on for scrying-pool:stateChanged', () => {
      manager.init();
      expect(hooksStub.on).toHaveBeenCalledWith(
        'scrying-pool:stateChanged',
        expect.any(Function)
      );
    });
  });

  // ── AC-6: _onStateChanged — track-disable strategy ────────────────────────

  describe('_onStateChanged() track-disable strategy (AC-6)', () => {
    let webrtcMock;

    beforeEach(() => {
      webrtcMock = { disableTrack: vi.fn(), enableTrack: vi.fn() };
      const trackDisableAdapter = createFoundryAdapterMock({
        webrtc: webrtcMock,
        settings: { get: (key) => (key === 'webrtcMode' ? 'track-disable' : null) },
        hooks: hooksStub,
      });
      manager = new VisibilityManager(stateStore, trackDisableAdapter);
      manager.init();
    });

    it('calls disableTrack(userId) when state is hidden', () => {
      const handler = hooksStub.on.mock.calls[0][1];
      handler({ userId: 'user-1', state: 'hidden' });
      expect(webrtcMock.disableTrack).toHaveBeenCalledWith('user-1');
      expect(webrtcMock.enableTrack).not.toHaveBeenCalled();
    });

    it('calls enableTrack(userId) when state is active', () => {
      const handler = hooksStub.on.mock.calls[0][1];
      handler({ userId: 'user-1', state: 'active' });
      expect(webrtcMock.enableTrack).toHaveBeenCalledWith('user-1');
      expect(webrtcMock.disableTrack).not.toHaveBeenCalled();
    });
  });

  // ── AC-7: _onStateChanged — css-fallback / unsupported ────────────────────

  describe('_onStateChanged() css-fallback strategy (AC-7)', () => {
    it('performs no webrtc call and throws no error when mode is css-fallback', () => {
      const cssFallbackAdapter = createFoundryAdapterMock({
        settings: { get: (key) => (key === 'webrtcMode' ? 'css-fallback' : null) },
        hooks: hooksStub,
      });
      manager = new VisibilityManager(stateStore, cssFallbackAdapter);
      manager.init();

      const handler = hooksStub.on.mock.calls[0][1];
      expect(() => handler({ userId: 'user-1', state: 'hidden' })).not.toThrow();
    });

    it('performs no webrtc call and throws no error when mode is unsupported', () => {
      const unsupportedAdapter = createFoundryAdapterMock({
        settings: { get: (key) => (key === 'webrtcMode' ? 'unsupported' : null) },
        hooks: hooksStub,
      });
      manager = new VisibilityManager(stateStore, unsupportedAdapter);
      manager.init();

      const handler = hooksStub.on.mock.calls[0][1];
      expect(() => handler({ userId: 'user-1', state: 'hidden' })).not.toThrow();
    });
  });

  // ── AC-10: null webrtc guard ──────────────────────────────────────────────

  describe('_onStateChanged() null webrtc guard (AC-10)', () => {
    it('does not throw when adapter.webrtc is null in track-disable mode', () => {
      const nullWebrtcAdapter = createFoundryAdapterMock({
        webrtc: null,
        settings: { get: (key) => (key === 'webrtcMode' ? 'track-disable' : null) },
        hooks: hooksStub,
      });
      manager = new VisibilityManager(stateStore, nullWebrtcAdapter);
      manager.init();

      const handler = hooksStub.on.mock.calls[0][1];
      expect(() => handler({ userId: 'user-1', state: 'hidden' })).not.toThrow();
    });

    it('does not throw when adapter.webrtc is null with state active', () => {
      const nullWebrtcAdapter = createFoundryAdapterMock({
        webrtc: null,
        settings: { get: (key) => (key === 'webrtcMode' ? 'track-disable' : null) },
        hooks: hooksStub,
      });
      manager = new VisibilityManager(stateStore, nullWebrtcAdapter);
      manager.init();

      const handler = hooksStub.on.mock.calls[0][1];
      expect(() => handler({ userId: 'user-1', state: 'active' })).not.toThrow();
    });
  });

  // ── AC-9: onRevert() ─────────────────────────────────────────────────────

  describe('onRevert() (AC-9)', () => {
    /** @type {import('../../../src/contracts/pending-op.js').PendingOp} */
    const pendingOp = {
      opId: 'op-1',
      userId: 'user-1',
      targetState: 'hidden',
      previousState: 'active',
      issuedAt: 1000000,
      timeoutId: null,
    };

    it('calls stateStore.setVisibility with previousState to revert', () => {
      const setSpy = vi.spyOn(stateStore, 'setVisibility');
      manager.onRevert(pendingOp);
      expect(setSpy).toHaveBeenCalledWith('user-1', 'active');
    });

    it('calls adapter.notifications.warn with a [ScryingPool]-prefixed message', () => {
      const warnMock = vi.fn();
      const warnAdapter = createFoundryAdapterMock({
        notifications: { warn: warnMock, info: () => {}, error: () => {} },
        hooks: hooksStub,
      });
      manager = new VisibilityManager(stateStore, warnAdapter);

      manager.onRevert(pendingOp);

      expect(warnMock).toHaveBeenCalledOnce();
      expect(warnMock.mock.calls[0][0]).toMatch(/^\[ScryingPool\]/);
    });

    it('includes userId in the warning message', () => {
      const warnMock = vi.fn();
      const warnAdapter = createFoundryAdapterMock({
        notifications: { warn: warnMock, info: () => {}, error: () => {} },
        hooks: hooksStub,
      });
      manager = new VisibilityManager(stateStore, warnAdapter);

      manager.onRevert(pendingOp);

      expect(warnMock.mock.calls[0][0]).toContain('user-1');
    });

    it('does NOT call notifications.info (no success notification on revert)', () => {
      const infoMock = vi.fn();
      const noInfoAdapter = createFoundryAdapterMock({
        notifications: { warn: () => {}, info: infoMock, error: () => {} },
        hooks: hooksStub,
      });
      manager = new VisibilityManager(stateStore, noInfoAdapter);

      manager.onRevert(pendingOp);

      expect(infoMock).not.toHaveBeenCalled();
    });

    it('does NOT call notifications.error', () => {
      const errorMock = vi.fn();
      const noErrorAdapter = createFoundryAdapterMock({
        notifications: { warn: () => {}, info: () => {}, error: errorMock },
        hooks: hooksStub,
      });
      manager = new VisibilityManager(stateStore, noErrorAdapter);

      manager.onRevert(pendingOp);

      expect(errorMock).not.toHaveBeenCalled();
    });
  });

  // ── teardown() — listener cleanup (T-debt deferred from 1.4) ──────────────

  describe('teardown()', () => {
    it('unregisters the stateChanged hook listener', () => {
      const hookId = 77;
      adapter.hooks.on = vi.fn().mockReturnValue(hookId);
      manager.init();

      manager.teardown();

      expect(adapter.hooks.off).toHaveBeenCalledWith('scrying-pool:stateChanged', hookId);
    });

    it('nulls _stateChangedHookId after teardown', () => {
      adapter.hooks.on = vi.fn().mockReturnValue(99);
      manager.init();
      manager.teardown();
      expect(manager._stateChangedHookId).toBeNull();
    });

    it('is safe to call before init()', () => {
      expect(() => manager.teardown()).not.toThrow();
    });

    it('does not call hooks.off when init was never called', () => {
      manager.teardown();
      expect(adapter.hooks.off).not.toHaveBeenCalled();
    });
  });
});
