/**
 * Unit tests for Design Tokens Module (Phase 5.4)
 *
 * Tests cover:
 * - token-resolver: var() parsing and formatting
 * - token-detector: CSSOM scanning (mocked)
 * - design-tokens-service: caching and query
 *
 * Note: jsdom doesn't have full CSSOM support, so we mock stylesheet APIs.
 */

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import {
  createTokenResolver,
  createTokenDetector,
  createDesignTokensService,
  type CssVarName,
} from '@/entrypoints/web-editor/core/design-tokens';

// =============================================================================
// Test Setup
// =============================================================================

beforeEach(() => {
  document.body.innerHTML = '';
  vi.restoreAllMocks();
});

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

// =============================================================================
// Token Resolver Tests
// =============================================================================

describe('token-resolver: formatCssVar', () => {
  it('formats simple var() without fallback', () => {
    const resolver = createTokenResolver();
    expect(resolver.formatCssVar('--color-primary')).toBe('var(--color-primary)');
  });

  it('formats var() with fallback', () => {
    const resolver = createTokenResolver();
    expect(resolver.formatCssVar('--color-primary', 'blue')).toBe('var(--color-primary, blue)');
  });

  it('trims fallback whitespace', () => {
    const resolver = createTokenResolver();
    expect(resolver.formatCssVar('--spacing', '  16px  ')).toBe('var(--spacing, 16px)');
  });

  it('ignores empty fallback', () => {
    const resolver = createTokenResolver();
    expect(resolver.formatCssVar('--x', '')).toBe('var(--x)');
    expect(resolver.formatCssVar('--x', '   ')).toBe('var(--x)');
  });
});

describe('token-resolver: parseCssVar', () => {
  it('parses simple var()', () => {
    const resolver = createTokenResolver();
    const result = resolver.parseCssVar('var(--color)');
    expect(result).toEqual({ name: '--color' });
  });

  it('parses var() with fallback', () => {
    const resolver = createTokenResolver();
    const result = resolver.parseCssVar('var(--color, blue)');
    expect(result).toEqual({ name: '--color', fallback: 'blue' });
  });

  it('parses var() with complex fallback', () => {
    const resolver = createTokenResolver();
    const result = resolver.parseCssVar('var(--color, rgba(0, 0, 0, 0.5))');
    expect(result).toEqual({ name: '--color', fallback: 'rgba(0, 0, 0, 0.5)' });
  });

  it('parses var() with nested var() fallback', () => {
    const resolver = createTokenResolver();
    const result = resolver.parseCssVar('var(--color, var(--fallback))');
    expect(result).toEqual({ name: '--color', fallback: 'var(--fallback)' });
  });

  it('returns null for non-var values', () => {
    const resolver = createTokenResolver();
    expect(resolver.parseCssVar('blue')).toBeNull();
    expect(resolver.parseCssVar('rgb(0,0,0)')).toBeNull();
    expect(resolver.parseCssVar('')).toBeNull();
    expect(resolver.parseCssVar('  ')).toBeNull();
  });

  it('returns null for invalid var()', () => {
    const resolver = createTokenResolver();
    expect(resolver.parseCssVar('var()')).toBeNull();
    expect(resolver.parseCssVar('var(invalid)')).toBeNull(); // No -- prefix
    expect(resolver.parseCssVar('var(--')).toBeNull(); // Unclosed
  });

  it('handles whitespace in var()', () => {
    const resolver = createTokenResolver();
    expect(resolver.parseCssVar('  var(  --x  )  ')).toEqual({ name: '--x' });
    expect(resolver.parseCssVar('var( --x , blue )')).toEqual({
      name: '--x',
      fallback: 'blue',
    });
  });
});

describe('token-resolver: extractCssVarNames', () => {
  it('extracts single var() reference', () => {
    const resolver = createTokenResolver();
    expect(resolver.extractCssVarNames('var(--color)')).toEqual(['--color']);
  });

  it('extracts multiple var() references', () => {
    const resolver = createTokenResolver();
    const names = resolver.extractCssVarNames('calc(var(--a) + var(--b)) var(--c)');
    expect(names).toEqual(['--a', '--b', '--c']);
  });

  it('returns empty array for no vars', () => {
    const resolver = createTokenResolver();
    expect(resolver.extractCssVarNames('blue')).toEqual([]);
    expect(resolver.extractCssVarNames('')).toEqual([]);
  });

  it('handles nested var() in fallback', () => {
    const resolver = createTokenResolver();
    // Only extracts top-level names (regex limitation, but good enough for Phase 5.4)
    const names = resolver.extractCssVarNames('var(--color, var(--fallback))');
    expect(names).toContain('--color');
    expect(names).toContain('--fallback');
  });
});

describe('token-resolver: readComputedValue', () => {
  it('reads custom property from element', () => {
    const div = document.createElement('div');
    div.style.setProperty('--test-color', 'red');
    document.body.append(div);

    const resolver = createTokenResolver();
    // Note: jsdom may not fully support computed custom properties
    // This test verifies the API works without errors
    const value = resolver.readComputedValue(div, '--test-color');
    // jsdom returns empty for custom props, but in real browser it would work
    expect(typeof value).toBe('string');
  });

  it('returns empty string for unset property', () => {
    const div = document.createElement('div');
    document.body.append(div);

    const resolver = createTokenResolver();
    expect(resolver.readComputedValue(div, '--nonexistent')).toBe('');
  });
});

describe('token-resolver: resolveToken', () => {
  it('returns available for set token', () => {
    const div = document.createElement('div');
    document.body.append(div);

    // Mock getComputedStyle
    vi.spyOn(window, 'getComputedStyle').mockReturnValue({
      getPropertyValue: (name: string) => (name === '--color' ? 'red' : ''),
    } as unknown as CSSStyleDeclaration);

    const resolver = createTokenResolver();
    const result = resolver.resolveToken(div, '--color');

    expect(result.token).toBe('--color');
    expect(result.computedValue).toBe('red');
    expect(result.availability).toBe('available');
  });

  it('returns unset for missing token', () => {
    const div = document.createElement('div');
    document.body.append(div);

    vi.spyOn(window, 'getComputedStyle').mockReturnValue({
      getPropertyValue: () => '',
    } as unknown as CSSStyleDeclaration);

    const resolver = createTokenResolver();
    const result = resolver.resolveToken(div, '--missing');

    expect(result.availability).toBe('unset');
  });
});

describe('token-resolver: resolveTokenForProperty', () => {
  it('builds CSS value for property', () => {
    const div = document.createElement('div');
    document.body.append(div);

    const resolver = createTokenResolver();
    const result = resolver.resolveTokenForProperty(div, '--color', 'color');

    expect(result.token).toBe('--color');
    expect(result.cssProperty).toBe('color');
    expect(result.cssValue).toBe('var(--color)');
    expect(result.method).toBe('computed');
  });

  it('includes fallback in CSS value', () => {
    const div = document.createElement('div');

    const resolver = createTokenResolver();
    const result = resolver.resolveTokenForProperty(div, '--color', 'background-color', {
      fallback: 'white',
    });

    expect(result.cssValue).toBe('var(--color, white)');
  });
});

// =============================================================================
// Token Detector Tests
// =============================================================================

describe('token-detector: collectInlineTokenNames', () => {
  it('collects token names from element inline style', () => {
    const div = document.createElement('div');
    div.style.setProperty('--custom-var', '10px');
    div.style.setProperty('color', 'red'); // Regular property, should be ignored
    document.body.append(div);

    const detector = createTokenDetector();
    const names = detector.collectInlineTokenNames(div);

    expect(names.has('--custom-var' as CssVarName)).toBe(true);
    expect(names.size).toBe(1);
  });

  it('collects from ancestor chain', () => {
    const parent = document.createElement('div');
    parent.style.setProperty('--parent-var', '20px');

    const child = document.createElement('div');
    child.style.setProperty('--child-var', '10px');

    parent.append(child);
    document.body.append(parent);

    const detector = createTokenDetector();
    const names = detector.collectInlineTokenNames(child);

    expect(names.has('--parent-var' as CssVarName)).toBe(true);
    expect(names.has('--child-var' as CssVarName)).toBe(true);
  });

  it('respects maxDepth option', () => {
    const grandparent = document.createElement('div');
    grandparent.style.setProperty('--grandparent-var', '30px');

    const parent = document.createElement('div');
    parent.style.setProperty('--parent-var', '20px');

    const child = document.createElement('div');
    child.style.setProperty('--child-var', '10px');

    grandparent.append(parent);
    parent.append(child);
    document.body.append(grandparent);

    const detector = createTokenDetector();
    const names = detector.collectInlineTokenNames(child, { maxDepth: 1 });

    // Should only include child and parent (depth 0 and 1)
    expect(names.has('--child-var' as CssVarName)).toBe(true);
    expect(names.has('--parent-var' as CssVarName)).toBe(true);
    expect(names.has('--grandparent-var' as CssVarName)).toBe(false);
  });

  it('returns empty set for element without custom props', () => {
    const div = document.createElement('div');
    div.style.color = 'red';
    document.body.append(div);

    const detector = createTokenDetector();
    const names = detector.collectInlineTokenNames(div);

    expect(names.size).toBe(0);
  });
});

describe('token-detector: collectRootIndex', () => {
  it('returns empty index when no stylesheets', () => {
    // jsdom has empty styleSheets by default
    const detector = createTokenDetector();
    const index = detector.collectRootIndex(document);

    expect(index.rootType).toBe('document');
    expect(index.tokens.size).toBe(0);
    expect(index.warnings).toEqual([]);
    expect(index.stats.styleSheets).toBeGreaterThanOrEqual(0);
  });

  it('handles missing styleSheets gracefully', () => {
    const detector = createTokenDetector();

    // Mock document with no styleSheets
    const mockRoot = {
      styleSheets: null,
      adoptedStyleSheets: undefined,
    } as unknown as Document;

    const index = detector.collectRootIndex(mockRoot);
    expect(index.tokens.size).toBe(0);
  });
});

// =============================================================================
// Design Tokens Service Tests
// =============================================================================

describe('design-tokens-service: basic operations', () => {
  it('creates service successfully', () => {
    const service = createDesignTokensService();
    expect(service).toBeDefined();
    expect(typeof service.getRootTokens).toBe('function');
    expect(typeof service.getContextTokens).toBe('function');
    service.dispose();
  });

  it('getRootTokens returns empty for document without tokens', () => {
    const service = createDesignTokensService();
    const result = service.getRootTokens(document);

    expect(result.tokens).toEqual([]);
    expect(result.warnings).toBeDefined();
    expect(result.stats).toBeDefined();

    service.dispose();
  });

  it('getContextTokens filters to available tokens', () => {
    const div = document.createElement('div');
    document.body.append(div);

    // Mock to return no tokens
    vi.spyOn(window, 'getComputedStyle').mockReturnValue({
      getPropertyValue: () => '',
    } as unknown as CSSStyleDeclaration);

    const service = createDesignTokensService();
    const result = service.getContextTokens(div);

    // Should be empty since no tokens resolve
    expect(result.tokens).toEqual([]);

    service.dispose();
  });
});

describe('design-tokens-service: utility methods', () => {
  it('formatCssVar delegates to resolver', () => {
    const service = createDesignTokensService();
    expect(service.formatCssVar('--x')).toBe('var(--x)');
    expect(service.formatCssVar('--x', 'y')).toBe('var(--x, y)');
    service.dispose();
  });

  it('parseCssVar delegates to resolver', () => {
    const service = createDesignTokensService();
    expect(service.parseCssVar('var(--x)')).toEqual({ name: '--x' });
    expect(service.parseCssVar('invalid')).toBeNull();
    service.dispose();
  });

  it('extractCssVarNames delegates to resolver', () => {
    const service = createDesignTokensService();
    expect(service.extractCssVarNames('var(--a) var(--b)')).toEqual(['--a', '--b']);
    service.dispose();
  });
});

describe('design-tokens-service: cache invalidation', () => {
  it('invalidateRoot clears cache and emits event', () => {
    const service = createDesignTokensService();
    const handler = vi.fn();

    service.onInvalidation(handler);
    service.invalidateRoot(document, 'manual');

    expect(handler).toHaveBeenCalledTimes(1);
    expect(handler).toHaveBeenCalledWith(
      expect.objectContaining({
        root: document,
        rootType: 'document',
        reason: 'manual',
      }),
    );

    service.dispose();
  });

  it('onInvalidation returns unsubscribe function', () => {
    const service = createDesignTokensService();
    const handler = vi.fn();

    const unsubscribe = service.onInvalidation(handler);
    service.invalidateRoot(document);
    expect(handler).toHaveBeenCalledTimes(1);

    unsubscribe();
    service.invalidateRoot(document);
    expect(handler).toHaveBeenCalledTimes(1); // Not called again

    service.dispose();
  });
});

describe('design-tokens-service: dispose', () => {
  it('can be called multiple times safely', () => {
    const service = createDesignTokensService();
    expect(() => {
      service.dispose();
      service.dispose();
    }).not.toThrow();
  });

  it('clears invalidation listeners on dispose', () => {
    const service = createDesignTokensService();
    const handler = vi.fn();

    service.onInvalidation(handler);
    service.dispose();

    // After dispose, invalidation shouldn't call handler
    // (but we can't easily test this without exposing internals)
    expect(handler).not.toHaveBeenCalled();
  });
});

describe('design-tokens-service: resolveToken', () => {
  it('resolves token for element', () => {
    const div = document.createElement('div');
    document.body.append(div);

    vi.spyOn(window, 'getComputedStyle').mockReturnValue({
      getPropertyValue: (name: string) => (name === '--color' ? '#ff0000' : ''),
    } as unknown as CSSStyleDeclaration);

    const service = createDesignTokensService();
    const result = service.resolveToken(div, '--color');

    expect(result.token).toBe('--color');
    expect(result.computedValue).toBe('#ff0000');
    expect(result.availability).toBe('available');

    service.dispose();
  });
});

describe('design-tokens-service: resolveTokenForProperty', () => {
  it('builds CSS value for applying token', () => {
    const div = document.createElement('div');

    const service = createDesignTokensService();
    const result = service.resolveTokenForProperty(div, '--spacing', 'padding', {
      fallback: '8px',
    });

    expect(result.cssValue).toBe('var(--spacing, 8px)');
    expect(result.cssProperty).toBe('padding');

    service.dispose();
  });
});
