/**
 * Unit tests for Web Editor V2 locator utilities.
 *
 * These tests run in jsdom and validate:
 * - Fingerprint generation (tag, id, class, text normalization)
 * - DOM path computation
 * - Selector candidate strategies (ID > data-attrs > classes > path > anchor)
 * - Locator creation and resolution
 * - Locator key stability
 * - Shadow host chain detection
 */

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

import type { ElementLocator } from '@/common/web-editor-types';
import {
  computeDomPath,
  computeFingerprint,
  createElementLocator,
  generateCssSelector,
  generateSelectorCandidates,
  getShadowHostChain,
  locateElement,
  locatorKey,
} from '@/entrypoints/web-editor/core/locator';

// =============================================================================
// Test Utilities
// =============================================================================

const supportsShadowDom =
  typeof (document.createElement('div') as HTMLElement).attachShadow === 'function';
const itIfShadow = supportsShadowDom ? it : it.skip;

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

// =============================================================================
// computeFingerprint Tests
// =============================================================================

describe('locator: computeFingerprint', () => {
  it('includes tag, id, class list, and normalized text', () => {
    const el = document.createElement('button');
    el.id = 'save';
    el.className = 'btn primary';
    el.textContent = '  Hello   world \n ok  ';
    document.body.append(el);

    const fp = computeFingerprint(el);

    expect(fp).toContain('button');
    expect(fp).toContain('id=save');
    expect(fp).toContain('class=btn.primary');
    expect(fp).toContain('text=Hello world ok');
  });

  it('limits classes to 8 tokens', () => {
    const el = document.createElement('div');
    el.className = Array.from({ length: 10 }, (_, i) => `c${i}`).join(' ');
    document.body.append(el);

    const fp = computeFingerprint(el);
    const classPart = fp.split('|').find((p) => p.startsWith('class='));

    // Should have exactly 8 classes
    const classes = classPart?.replace('class=', '').split('.') ?? [];
    expect(classes).toHaveLength(8);
    expect(classes).toEqual(['c0', 'c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7']);
  });

  it('truncates text to 32 characters', () => {
    const el = document.createElement('div');
    el.textContent = 'a'.repeat(40);
    document.body.append(el);

    const fp = computeFingerprint(el);
    const textPart = fp.split('|').find((p) => p.startsWith('text='));
    const text = textPart?.replace('text=', '') ?? '';

    expect(text.length).toBeLessThanOrEqual(32);
  });

  it('returns only tag when id/class/text are empty', () => {
    const el = document.createElement('div');
    document.body.append(el);

    expect(computeFingerprint(el)).toBe('div');
  });

  it('normalizes whitespace in text', () => {
    const el = document.createElement('span');
    el.textContent = '\n  foo   bar\t\tbaz  \n';
    document.body.append(el);

    const fp = computeFingerprint(el);
    expect(fp).toContain('text=foo bar baz');
  });

  it('preserves class order from classList', () => {
    const el = document.createElement('div');
    el.className = 'z-class a-class m-class';
    document.body.append(el);

    const fp = computeFingerprint(el);
    // Classes are preserved in their original order from classList
    expect(fp).toContain('class=z-class.a-class.m-class');
  });
});

// =============================================================================
// computeDomPath Tests
// =============================================================================

describe('locator: computeDomPath', () => {
  it('computes stable indices for nested elements in document', () => {
    const container = document.createElement('div');
    const first = document.createElement('span');
    const second = document.createElement('span');
    container.append(first, second);
    document.body.append(container);

    const firstPath = computeDomPath(first);
    const secondPath = computeDomPath(second);

    // Second child should have higher last index
    expect(secondPath[secondPath.length - 1]).toBeGreaterThan(
      firstPath[firstPath.length - 1] as number,
    );
  });

  it('returns different paths for siblings', () => {
    const a = document.createElement('div');
    const b = document.createElement('div');
    document.body.append(a, b);

    expect(computeDomPath(a)).not.toEqual(computeDomPath(b));
  });

  itIfShadow('computes index within a ShadowRoot boundary', () => {
    const host = document.createElement('div');
    document.body.append(host);

    const shadow = host.attachShadow({ mode: 'open' });
    const a = document.createElement('div');
    const b = document.createElement('div');
    shadow.append(a, b);

    // Path within shadow should start from 0
    const pathA = computeDomPath(a);
    const pathB = computeDomPath(b);

    expect(pathA[0]).toBe(0);
    expect(pathB[0]).toBe(1);
  });
});

// =============================================================================
// Selector Generation Tests
// =============================================================================

describe('locator: generateSelectorCandidates', () => {
  it('prefers unique id selector first', () => {
    const el = document.createElement('div');
    el.id = 'unique';
    document.body.append(el);

    const candidates = generateSelectorCandidates(el, { root: document });
    expect(candidates[0]).toBe('#unique');
  });

  it('uses data-testid when unique', () => {
    const el = document.createElement('button');
    el.setAttribute('data-testid', 'save-btn');
    document.body.append(el);

    const candidates = generateSelectorCandidates(el, { root: document });
    expect(candidates[0]).toBe('[data-testid="save-btn"]');
  });

  it('uses tag+data-testid when attribute alone is not unique', () => {
    const div = document.createElement('div');
    div.setAttribute('data-testid', 'dup');
    const span = document.createElement('span');
    span.setAttribute('data-testid', 'dup');
    document.body.append(div, span);

    const candidates = generateSelectorCandidates(div, { root: document });
    expect(candidates[0]).toBe('div[data-testid="dup"]');
  });

  it('uses tag+class when class alone is not unique', () => {
    const a = document.createElement('div');
    a.className = 'item';
    const b = document.createElement('button');
    b.className = 'item';
    document.body.append(a, b);

    const candidates = generateSelectorCandidates(b, { root: document });
    expect(candidates[0]).toBe('button.item');
  });

  it('uses class pair selector when only the combination is unique', () => {
    const target = document.createElement('div');
    target.className = 'a b';
    const onlyA = document.createElement('div');
    onlyA.className = 'a';
    const onlyB = document.createElement('div');
    onlyB.className = 'b';
    document.body.append(target, onlyA, onlyB);

    const candidates = generateSelectorCandidates(target, { root: document });
    expect(candidates[0]).toBe('.a.b');
  });

  it('generates multiple candidates', () => {
    const el = document.createElement('div');
    el.id = 'myid';
    el.className = 'myclass';
    el.setAttribute('data-testid', 'mytest');
    document.body.append(el);

    const candidates = generateSelectorCandidates(el, { root: document, maxCandidates: 5 });
    expect(candidates.length).toBeGreaterThan(1);
    expect(candidates).toContain('#myid');
  });

  it('falls back to structural path selector when no unique attrs/classes exist', () => {
    const section = document.createElement('section');
    const p = document.createElement('p');
    section.append(p);
    document.body.append(section);

    const candidates = generateSelectorCandidates(p, { root: document });
    // Should include a path-based selector
    const hasPath = candidates.some((c) => c.includes('>'));
    expect(hasPath).toBe(true);
  });

  it('respects maxCandidates option', () => {
    const el = document.createElement('div');
    el.id = 'test';
    el.className = 'a b c';
    el.setAttribute('data-testid', 'x');
    document.body.append(el);

    const candidates = generateSelectorCandidates(el, { root: document, maxCandidates: 2 });
    expect(candidates.length).toBeLessThanOrEqual(2);
  });
});

describe('locator: generateCssSelector', () => {
  it('returns the best single selector', () => {
    const el = document.createElement('div');
    el.id = 'unique';
    document.body.append(el);

    expect(generateCssSelector(el, { root: document })).toBe('#unique');
  });

  it('returns empty string for orphan element', () => {
    const el = document.createElement('div');
    // Element not in document
    const selector = generateCssSelector(el);
    // May return a selector or empty depending on implementation
    expect(typeof selector).toBe('string');
  });
});

// =============================================================================
// Locator Creation & Resolution Tests
// =============================================================================

describe('locator: createElementLocator', () => {
  it('creates a locator with selectors, fingerprint, and dom path', () => {
    const el = document.createElement('div');
    el.id = 'target';
    el.className = 'box';
    el.textContent = 'Hello';
    document.body.append(el);

    const locator = createElementLocator(el);

    expect(locator.selectors.length).toBeGreaterThan(0);
    expect(locator.selectors[0]).toBe('#target');
    expect(locator.fingerprint).toBe(computeFingerprint(el));
    expect(locator.path).toEqual(computeDomPath(el));
  });

  itIfShadow('includes shadowHostChain when element is inside Shadow DOM', () => {
    const host = document.createElement('div');
    host.id = 'host';
    document.body.append(host);

    const shadow = host.attachShadow({ mode: 'open' });
    const target = document.createElement('span');
    target.id = 'inner';
    shadow.append(target);

    const locator = createElementLocator(target);

    expect(locator.shadowHostChain).toBeDefined();
    expect(locator.shadowHostChain!.length).toBeGreaterThan(0);
  });
});

describe('locator: locateElement', () => {
  it('locates an element from its own locator', () => {
    const el = document.createElement('div');
    el.id = 'target';
    el.textContent = 'Hello';
    document.body.append(el);

    const locator = createElementLocator(el);
    const found = locateElement(locator, document);

    expect(found).toBe(el);
  });

  it('tries multiple selectors and falls back to later candidates', () => {
    const el = document.createElement('div');
    el.id = 'target';
    document.body.append(el);

    const locator: ElementLocator = {
      selectors: ['#missing', '#target'],
      fingerprint: computeFingerprint(el),
      path: [],
    };

    expect(locateElement(locator, document)).toBe(el);
  });

  it('returns null when selector is not unique', () => {
    const a = document.createElement('div');
    a.className = 'x';
    const b = document.createElement('div');
    b.className = 'x';
    document.body.append(a, b);

    const locator: ElementLocator = {
      selectors: ['.x'],
      fingerprint: computeFingerprint(a),
      path: [],
    };

    // Should return null because .x matches 2 elements
    expect(locateElement(locator, document)).toBeNull();
  });

  it('returns null when fingerprint does not match', () => {
    const el = document.createElement('div');
    el.id = 'a';
    document.body.append(el);

    const locator: ElementLocator = {
      selectors: ['#a'],
      fingerprint: 'div|id=wrong', // Wrong fingerprint
      path: [],
    };

    expect(locateElement(locator, document)).toBeNull();
  });

  it('handles element removal gracefully', () => {
    const el = document.createElement('div');
    el.id = 'temp';
    document.body.append(el);

    const locator = createElementLocator(el);
    el.remove();

    expect(locateElement(locator, document)).toBeNull();
  });

  itIfShadow('locates element inside nested ShadowRoot via shadowHostChain', () => {
    const outerHost = document.createElement('div');
    outerHost.id = 'outer-host';
    document.body.append(outerHost);

    const outerShadow = outerHost.attachShadow({ mode: 'open' });

    const innerHost = document.createElement('div');
    innerHost.id = 'inner-host';
    outerShadow.append(innerHost);

    const innerShadow = innerHost.attachShadow({ mode: 'open' });
    const target = document.createElement('span');
    target.id = 'shadow-target';
    innerShadow.append(target);

    const locator = createElementLocator(target);
    const found = locateElement(locator, document);

    expect(found).toBe(target);
  });
});

// =============================================================================
// Shadow Host Chain Tests
// =============================================================================

describe('locator: getShadowHostChain', () => {
  it('returns undefined for element not in Shadow DOM', () => {
    const el = document.createElement('div');
    document.body.append(el);

    expect(getShadowHostChain(el)).toBeUndefined();
  });

  itIfShadow('returns chain for single-level Shadow DOM', () => {
    const host = document.createElement('div');
    host.id = 'myhost';
    document.body.append(host);

    const shadow = host.attachShadow({ mode: 'open' });
    const inner = document.createElement('span');
    shadow.append(inner);

    const chain = getShadowHostChain(inner);
    expect(chain).toBeDefined();
    expect(chain!.length).toBe(1);
    expect(chain![0]).toBe('#myhost');
  });

  itIfShadow('returns chain for nested Shadow DOMs', () => {
    const outer = document.createElement('div');
    outer.id = 'outer';
    document.body.append(outer);

    const outerShadow = outer.attachShadow({ mode: 'open' });
    const inner = document.createElement('div');
    inner.id = 'inner';
    outerShadow.append(inner);

    const innerShadow = inner.attachShadow({ mode: 'open' });
    const target = document.createElement('span');
    innerShadow.append(target);

    const chain = getShadowHostChain(target);
    expect(chain).toBeDefined();
    expect(chain!.length).toBe(2);
    expect(chain![0]).toBe('#outer');
    expect(chain![1]).toBe('#inner');
  });
});

// =============================================================================
// locatorKey Tests
// =============================================================================

describe('locator: locatorKey', () => {
  it('generates a stable key including selectors', () => {
    const el = document.createElement('div');
    el.id = 'k1';
    document.body.append(el);

    const locator = createElementLocator(el);
    const key = locatorKey(locator);

    expect(key).toContain('sel:');
    expect(key).toContain('#k1');
  });

  it('differs for different locators', () => {
    const a = document.createElement('div');
    a.id = 'a';
    const b = document.createElement('div');
    b.id = 'b';
    document.body.append(a, b);

    const keyA = locatorKey(createElementLocator(a));
    const keyB = locatorKey(createElementLocator(b));

    expect(keyA).not.toBe(keyB);
  });

  it('is deterministic for same element', () => {
    const el = document.createElement('div');
    el.id = 'stable';
    document.body.append(el);

    const key1 = locatorKey(createElementLocator(el));
    const key2 = locatorKey(createElementLocator(el));

    expect(key1).toBe(key2);
  });

  itIfShadow('includes shadow host chain in the key when present', () => {
    const host = document.createElement('div');
    host.id = 'host';
    document.body.append(host);
    const shadow = host.attachShadow({ mode: 'open' });

    const target = document.createElement('span');
    target.id = 't';
    shadow.append(target);

    const locator = createElementLocator(target);
    const key = locatorKey(locator);

    expect(key).toContain('shadow:');
    expect(key).toContain('#host');
  });
});
