/**
 * ConfirmationBar unit tests.
 *
 * Story 3.2: Scene Auto-Apply & ConfirmationBar
 * Import rule: tests may import from src/ but test files themselves are not subject to
 * the src/ import boundary rules (they're in tests/).
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { ConfirmationBar } from '../../../../src/ui/gm/ConfirmationBar.js';

// Test helper: create a mock FoundryAdapter surface
function createMockAdapter(overrides = {}) {
  return {
    hooks: {
      on: vi.fn(() => 42),
      off: vi.fn(),
      callAll: vi.fn(),
      ...overrides.hooks,
    },
    i18n: {
      localize: vi.fn((key) => {
        // For testing, return strings with placeholders that match ConfirmationBar's .replace() calls
        const translations = {
          'scrying-pool.presets.confirmation.applied': 'Preset applied — {name}',
          'scrying-pool.presets.confirmation.counts': '{hidden} hidden, {visible} visible',
          'scrying-pool.presets.confirmation.partial-fail': '(some updates pending)',
          'scrying-pool.presets.confirmation.undo': 'Undo preset apply',
        };
        return translations[key] ?? key;
      }),
      ...overrides.i18n,
    },
    ...overrides,
  };
}

// Test helper: create a mock VisibilityManager surface
function createMockVisibilityManager(overrides = {}) {
  return {
    applyMatrix: vi.fn().mockResolvedValue({}),
    getMatrix: vi.fn(() => ({ _version: 1, matrix: {} })),
    ...overrides,
  };
}

// Test helper: create a mock SocketHandler surface
function createMockSocketHandler(overrides = {}) {
  return {
    emit: vi.fn(),
    ...overrides,
  };
}

// Test helper: create a mock StripOverlayLayer surface
function createMockStripOverlayLayer(overrides = {}) {
  const mockElement = document.createElement('div');
  mockElement.className = 'sp-strip__overlay-layer';
  mockElement.style.cssText = 'position: absolute; inset: 0; pointer-events: none; overflow: visible;';
  
  return {
    get element() {
      return mockElement;
    },
    render: vi.fn((content) => {
      const container = document.createElement('div');
      container.style.pointerEvents = 'auto';
      container.innerHTML = content;
      mockElement.appendChild(container);
      return container;
    }),
    clearAll: vi.fn(),
    ...overrides,
  };
}

// Test helper: create a mock matrix
function createMockMatrix() {
  return { _version: 1, matrix: { user1: 'active', user2: 'hidden', user3: 'active' } };
}

describe('ConfirmationBar', () => {
  let adapter;
  let visibilityManager;
  let socketHandler;
  let stripOverlayLayer;
  let confirmationBar;
  let mockElement;

  beforeEach(() => {
    vi.useFakeTimers();
    
    adapter = createMockAdapter();
    visibilityManager = createMockVisibilityManager();
    socketHandler = createMockSocketHandler();
    
    mockElement = document.createElement('div');
    mockElement.className = 'scrying-pool__confirmation-bar';
    mockElement.style.display = 'none';
    
    stripOverlayLayer = createMockStripOverlayLayer({
      element: mockElement,
      render: vi.fn((content) => {
        mockElement.innerHTML = content;
        mockElement.style.display = 'block';
      }),
    });
    
    // Create confirmation bar with mock DOM
    document.body.appendChild(mockElement);
    
    confirmationBar = new ConfirmationBar(adapter, visibilityManager, socketHandler, stripOverlayLayer);
    confirmationBar.init();
  });

  afterEach(() => {
    vi.useRealTimers();
    confirmationBar?.teardown();
    if (mockElement && mockElement.parentNode) {
      mockElement.parentNode.removeChild(mockElement);
    }
    vi.clearAllMocks();
  });

  // --------------------------------------------------------------------------
  // Constructor Tests
  // --------------------------------------------------------------------------

  describe('constructor()', () => {
    it('STORY32-TDD: should accept all required dependencies', () => {
      expect(() => {
        new ConfirmationBar(adapter, visibilityManager, socketHandler, stripOverlayLayer);
      }).not.toThrow();
    });

    it('STORY32-TDD: should store dependencies', () => {
      expect(confirmationBar._adapter).toBe(adapter);
      expect(confirmationBar._visibilityManager).toBe(visibilityManager);
      expect(confirmationBar._socketHandler).toBe(socketHandler);
      expect(confirmationBar._stripOverlayLayer).toBe(stripOverlayLayer);
    });

    it('STORY32-TDD: should initialize with empty state', () => {
      expect(confirmationBar._previousMatrix).toBeNull();
      expect(confirmationBar._dismissTimer).toBeNull();
      expect(confirmationBar._isVisible).toBe(false);
    });
  });

  // --------------------------------------------------------------------------
  // init() Tests
  // --------------------------------------------------------------------------

  describe('init()', () => {
    it('STORY32-TDD: should register hook listener for scrying-pool:presetApplied', () => {
      expect(adapter.hooks.on).toHaveBeenCalledWith(
        'scrying-pool:presetApplied',
        expect.any(Function)
      );
    });

    it('STORY32-TDD: should be idempotent', () => {
      const initialCalls = adapter.hooks.on.mock.calls.length;
      confirmationBar.init();
      expect(adapter.hooks.on.mock.calls.length).toBe(initialCalls);
    });
  });

  // --------------------------------------------------------------------------
  // teardown() Tests
  // --------------------------------------------------------------------------

  describe('teardown()', () => {
    it('STORY32-TDD: should unregister hook listener', () => {
      confirmationBar.teardown();
      expect(adapter.hooks.off).toHaveBeenCalledWith(
        'scrying-pool:presetApplied',
        expect.any(Function)
      );
    });

    it('STORY32-TDD: should clear active timer', () => {
      confirmationBar._dismissTimer = setTimeout(() => {}, 1000);
      confirmationBar.teardown();
      expect(confirmationBar._dismissTimer).toBeNull();
    });

    it('STORY32-TDD: should be idempotent', () => {
      confirmationBar.teardown();
      expect(() => confirmationBar.teardown()).not.toThrow();
    });
  });

  // --------------------------------------------------------------------------
  // show() Tests
  // --------------------------------------------------------------------------

  describe('show()', () => {
    it('STORY32-TDD: should store previous matrix and payload', () => {
      const matrix = createMockMatrix();
      const payload = {
        presetName: 'Combat',
        matrix,
        autoApplied: true,
        timestamp: Date.now(),
      };
      
      confirmationBar.show(payload);
      
      expect(confirmationBar._previousMatrix).toEqual(matrix);
      expect(confirmationBar._lastPayload).toEqual(payload);
    });

    it('STORY32-TDD: should render bar in strip overlay', () => {
      const payload = {
        presetName: 'Combat',
        matrix: createMockMatrix(),
        autoApplied: true,
      };
      
      confirmationBar.show(payload);
      
      expect(stripOverlayLayer.render).toHaveBeenCalled();
      expect(confirmationBar._isVisible).toBe(true);
    });

    it('STORY32-TDD: should determine variant from payload', () => {
      // Test default variant
      const payload1 = {
        presetName: 'Combat',
        matrix: createMockMatrix(),
        autoApplied: true,
      };
      confirmationBar.show(payload1);
      expect(stripOverlayLayer.render.mock.calls[0][0]).toContain('default');
      
      // Test amber variant (partial fail)
      const payload2 = {
        presetName: 'Combat',
        matrix: createMockMatrix(),
        autoApplied: true,
        partialFail: true,
      };
      confirmationBar.show(payload2);
      expect(stripOverlayLayer.render.mock.calls[1][0]).toContain('amber');
    });

    it('STORY32-TDD: should calculate hidden/visible counts from matrix', () => {
      const matrix = {
        _version: 1,
        matrix: { user1: 'hidden', user2: 'hidden', user3: 'active', user4: 'active' }
      };
      const payload = {
        presetName: 'Combat',
        matrix,
        autoApplied: true,
      };
      
      confirmationBar.show(payload);
      
      const renderCall = stripOverlayLayer.render.mock.calls[0][0];
      expect(renderCall).toContain('2 hidden');
      expect(renderCall).toContain('2 visible');
    });

    it('STORY32-TDD: should start dismiss timer', () => {
      const payload = {
        presetName: 'Combat',
        matrix: createMockMatrix(),
        autoApplied: true,
      };
      
      confirmationBar.show(payload);
      
      expect(confirmationBar._dismissTimer).not.toBeNull();
    });

    it('STORY32-TDD: should use short duration for rapid successive applies', () => {
      const payload1 = {
        presetName: 'Combat',
        matrix: createMockMatrix(),
        autoApplied: true,
        timestamp: Date.now(),
      };
      const payload2 = {
        presetName: 'Theatre',
        matrix: createMockMatrix(),
        autoApplied: true,
        timestamp: Date.now(),
      };
      
      // First apply
      confirmationBar.show(payload1);
      const firstTimer = confirmationBar._dismissTimer;
      
      // Second apply within 60s - should use short duration
      vi.advanceTimersByTime(1000);
      confirmationBar.show(payload2);
      const secondTimer = confirmationBar._dismissTimer;
      
      expect(secondTimer).not.toBe(firstTimer);
    });
  });

  // --------------------------------------------------------------------------
  // hide() Tests
  // --------------------------------------------------------------------------

  describe('hide()', () => {
    beforeEach(() => {
      const payload = {
        presetName: 'Combat',
        matrix: createMockMatrix(),
        autoApplied: true,
      };
      confirmationBar.show(payload);
    });

    it('STORY32-TDD: should clear dismiss timer', () => {
      confirmationBar.hide();
      expect(confirmationBar._dismissTimer).toBeNull();
    });

    it('STORY32-TDD: should clear previous matrix', () => {
      confirmationBar.hide();
      expect(confirmationBar._previousMatrix).toBeNull();
    });

    it('STORY32-TDD: should set isVisible to false', () => {
      expect(confirmationBar._isVisible).toBe(true);
      confirmationBar.hide();
      expect(confirmationBar._isVisible).toBe(false);
    });
  });

  // --------------------------------------------------------------------------
  // _onUndo() Tests
  // --------------------------------------------------------------------------

  describe('_onUndo()', () => {
    it('STORY32-TDD: should revert to previous matrix', () => {
      const previousMatrix = createMockMatrix();
      confirmationBar._previousMatrix = previousMatrix;
      
      confirmationBar._onUndo();
      
      expect(visibilityManager.applyMatrix).toHaveBeenCalledWith(previousMatrix);
    });

    it('STORY32-TDD: should hide the bar', () => {
      const previousMatrix = createMockMatrix();
      confirmationBar._previousMatrix = previousMatrix;
      confirmationBar._isVisible = true;
      
      confirmationBar._onUndo();
      
      expect(confirmationBar._isVisible).toBe(false);
    });

    it('STORY32-TDD: should do nothing when no previous matrix', () => {
      confirmationBar._previousMatrix = null;
      
      expect(() => confirmationBar._onUndo()).not.toThrow();
      expect(visibilityManager.applyMatrix).not.toHaveBeenCalled();
    });

    it('STORY32-TDD: should emit hook for undo notification', () => {
      const previousMatrix = createMockMatrix();
      confirmationBar._previousMatrix = previousMatrix;
      
      confirmationBar._onUndo();
      
      expect(adapter.hooks.callAll).toHaveBeenCalledWith(
        'scrying-pool:presetUndo',
        expect.any(Object)
      );
    });
  });

  // --------------------------------------------------------------------------
  // _startDismissTimer() Tests
  // --------------------------------------------------------------------------

  describe('_startDismissTimer()', () => {
    it('STORY32-TDD: should use default duration (8000ms)', () => {
      confirmationBar._startDismissTimer();
      
      expect(confirmationBar._dismissTimer).not.toBeNull();
    });

    it('STORY32-TDD: should use short duration (4000ms) when recently active', () => {
      // Set last applied timestamp to recent
      confirmationBar._lastAppliedTimestamp = Date.now();
      confirmationBar._recentApplyCount = 2;
      
      confirmationBar._startDismissTimer();
      
      // Should use short duration
      expect(confirmationBar._dismissTimer).not.toBeNull();
    });

    it('STORY32-TDD: should call hide on timeout', () => {
      confirmationBar._startDismissTimer();
      
      vi.advanceTimersByTime(8000);
      
      expect(confirmationBar._isVisible).toBe(false);
      expect(confirmationBar._dismissTimer).toBeNull();
    });

    it('STORY32-TDD: should clear previous timer', () => {
      confirmationBar._startDismissTimer();
      const firstTimer = confirmationBar._dismissTimer;
      
      confirmationBar._startDismissTimer();
      const secondTimer = confirmationBar._dismissTimer;
      
      expect(secondTimer).not.toBe(firstTimer);
    });
  });

  // --------------------------------------------------------------------------
  // _onPresetApplied() Tests - Hook Handler
  // --------------------------------------------------------------------------

  describe('_onPresetApplied() [hook handler]', () => {
    it('STORY32-TDD: should show bar when not visible', () => {
      const payload = {
        presetName: 'Combat',
        matrix: createMockMatrix(),
        autoApplied: true,
      };
      
      // Trigger hook directly
      const handler = adapter.hooks.on.mock.calls.find(
        call => call[0] === 'scrying-pool:presetApplied'
      )[1];
      
      handler(payload);
      
      expect(confirmationBar._isVisible).toBe(true);
      expect(stripOverlayLayer.render).toHaveBeenCalled();
    });

    it('STORY32-TDD: should instant-replace when already visible', () => {
      const payload1 = {
        presetName: 'Combat',
        matrix: createMockMatrix(),
        autoApplied: true,
      };
      const payload2 = {
        presetName: 'Theatre',
        matrix: { _version: 1, matrix: { user1: 'hidden' } },
        autoApplied: true,
      };
      
      const handler = adapter.hooks.on.mock.calls.find(
        call => call[0] === 'scrying-pool:presetApplied'
      )[1];
      
      // First apply
      handler(payload1);
      const firstRender = stripOverlayLayer.render.mock.calls.length;
      
      // Second apply while visible - should instant-replace
      handler(payload2);
      
      // Should have rendered again (instant-replace)
      expect(stripOverlayLayer.render.mock.calls.length).toBeGreaterThan(firstRender);
    });

    it('STORY32-TDD: should track recent apply count', () => {
      const payload = {
        presetName: 'Combat',
        matrix: createMockMatrix(),
        autoApplied: true,
        timestamp: Date.now(),
      };
      
      const handler = adapter.hooks.on.mock.calls.find(
        call => call[0] === 'scrying-pool:presetApplied'
      )[1];
      
      // First apply
      handler(payload);
      expect(confirmationBar._recentApplyCount).toBe(1);
      
      // Second apply
      handler({ ...payload, timestamp: Date.now() });
      expect(confirmationBar._recentApplyCount).toBe(2);
    });
  });

  // --------------------------------------------------------------------------
  // Accessibility Tests
  // --------------------------------------------------------------------------

  describe('Accessibility', () => {
    it('STORY32-TDD: should set role and aria attributes on rendered bar', () => {
      const payload = {
        presetName: 'Combat',
        matrix: createMockMatrix(),
        autoApplied: true,
      };
      
      confirmationBar.show(payload);
      
      const renderCall = stripOverlayLayer.render.mock.calls[0][0];
      expect(renderCall).toContain('role="status"');
      expect(renderCall).toContain('aria-live="polite"');
      // aria-label will contain the i18n keys, but that's ok for testing the attribute exists
    });

    it('STORY32-TDD: should set aria-label on undo button', () => {
      const payload = {
        presetName: 'Combat',
        matrix: createMockMatrix(),
        autoApplied: true,
      };
      
      confirmationBar.show(payload);
      
      const renderCall = stripOverlayLayer.render.mock.calls[0][0];
      // aria-label will contain the i18n key, but that's ok for testing the attribute exists
      expect(renderCall).toContain('aria-label=');
      expect(renderCall).toContain('data-action="confirmation-bar-undo"');
    });

    it('STORY32-TDD: should use correct vocabulary from UX-DR17', () => {
      const payload = {
        presetName: 'Combat',
        matrix: createMockMatrix(),
        autoApplied: true,
      };
      
      confirmationBar.show(payload);
      
      const renderCall = stripOverlayLayer.render.mock.calls[0][0];
      // The message will contain i18n keys, but we're testing that it renders
      expect(renderCall).toContain('sp-confirmation-bar__message');
      expect(renderCall).toContain('sp-confirmation-bar__undo-btn');
    });
  });

  // --------------------------------------------------------------------------
  // Instant-Replace Rule Tests
  // --------------------------------------------------------------------------

  describe('Instant-Replace Rule', () => {
    it('STORY32-TDD: should replace without crossfade animation', () => {
      const payload1 = {
        presetName: 'Combat',
        matrix: createMockMatrix(),
        autoApplied: true,
      };
      const payload2 = {
        presetName: 'Theatre',
        matrix: createMockMatrix(),
        autoApplied: true,
      };
      
      confirmationBar.show(payload1);
      const firstContent = stripOverlayLayer.render.mock.calls[0][0];
      
      confirmationBar.show(payload2);
      const secondContent = stripOverlayLayer.render.mock.calls[1][0];
      
      // Content should be different (new preset)
      expect(firstContent).not.toEqual(secondContent);
    });

    it('STORY32-TDD: should maintain single bar instance', () => {
      const payload1 = {
        presetName: 'Combat',
        matrix: createMockMatrix(),
        autoApplied: true,
      };
      const payload2 = {
        presetName: 'Theatre',
        matrix: createMockMatrix(),
        autoApplied: true,
      };
      
      confirmationBar.show(payload1);
      confirmationBar.show(payload2);
      
      // Should still only have one bar visible at a time
      expect(confirmationBar._isVisible).toBe(true);
    });
  });

  // --------------------------------------------------------------------------
  // Edge Cases
  // --------------------------------------------------------------------------

  describe('Edge Cases', () => {
    it('STORY32-TDD: should handle null payload gracefully', () => {
      expect(() => confirmationBar.show(null)).not.toThrow();
    });

    it('STORY32-TDD: should handle undefined matrix', () => {
      const payload = {
        presetName: 'Combat',
        autoApplied: true,
      };
      
      expect(() => confirmationBar.show(payload)).not.toThrow();
    });

    it('STORY32-TDD: should handle empty matrix', () => {
      const payload = {
        presetName: 'Combat',
        matrix: { _version: 1, matrix: {} },
        autoApplied: true,
      };
      
      confirmationBar.show(payload);
      
      const renderCall = stripOverlayLayer.render.mock.calls[0][0];
      expect(renderCall).toContain('0 hidden');
      expect(renderCall).toContain('0 visible');
    });
  });
});
