// @ts-check
const { test, expect } = require('playwright/test');
const { BASE_URL, waitForSxReady, trackErrors } = require('./helpers');

// ─── Helpers ────────────────────────────────────────────────

/** Get SSR HTML before JS hydration by fetching raw page */
async function getSSRHtml(page, selector) {
  return page.evaluate(async (sel) => {
    const resp = await fetch(window.location.href);
    const html = await resp.text();
    const parser = new DOMParser();
    const doc = parser.parseFromString(html, 'text/html');
    const el = doc.querySelector(sel);
    return el ? el.innerHTML : null;
  }, selector);
}

/** Describe childNodes of an element as [{type, text}] */
async function describeChildren(page, selector) {
  return page.evaluate((sel) => {
    const el = document.querySelector(sel);
    if (!el) return null;
    const nodes = [];
    for (let i = 0; i < el.childNodes.length; i++) {
      const n = el.childNodes[i];
      nodes.push({ type: n.nodeType, text: n.textContent });
    }
    return nodes;
  }, selector);
}

/** Count comment nodes in an element */
async function countComments(page, selector) {
  return page.evaluate((sel) => {
    const el = document.querySelector(sel);
    if (!el) return -1;
    let count = 0;
    const walker = document.createTreeWalker(el, NodeFilter.SHOW_COMMENT);
    while (walker.nextNode()) count++;
    return count;
  }, selector);
}

// ─── DOM preservation tests ──────────────────────────────────

test.describe('DOM-preserving hydration', () => {

  test('islands hydrate without errors or warnings', async ({ page }) => {
    const errs = trackErrors(page);
    await page.goto(BASE_URL + '/sx/', { waitUntil: 'domcontentloaded' });
    await waitForSxReady(page);
    const bootLog = await page.evaluate(() => {
      // Check for hydrate fallback warnings
      return (window.__sxBootLog || []).filter(l => l.includes('fallback') || l.includes('FAILED'));
    });
    expect(bootLog).toHaveLength(0);
    expect(errs.errors()).toHaveLength(0);
  });

  test('both islands report hydrated in boot log', async ({ page }) => {
    const logs = [];
    page.on('console', msg => { if (msg.text().includes('[sx]')) logs.push(msg.text()); });
    await page.goto(BASE_URL + '/sx/', { waitUntil: 'domcontentloaded' });
    await waitForSxReady(page);
    const hydrated = logs.filter(l => l.includes('hydrated island:'));
    expect(hydrated.length).toBe(2);
    expect(hydrated.some(l => l.includes('layouts/header'))).toBeTruthy();
    expect(hydrated.some(l => l.includes('home/stepper'))).toBeTruthy();
  });

  test('no replaceChildren during hydration — SSR DOM preserved', async ({ page }) => {
    // Intercept replaceChildren calls on island elements
    await page.addInitScript(() => {
      window.__replaceChildrenCalls = 0;
      const orig = Element.prototype.replaceChildren;
      Element.prototype.replaceChildren = function(...args) {
        if (this.hasAttribute && this.hasAttribute('data-sx-island')) {
          window.__replaceChildrenCalls++;
        }
        return orig.apply(this, args);
      };
    });
    await page.goto(BASE_URL + '/sx/', { waitUntil: 'domcontentloaded' });
    await waitForSxReady(page);
    const calls = await page.evaluate(() => window.__replaceChildrenCalls);
    expect(calls).toBe(0);
  });

  test('no stray comment markers in island DOM', async ({ page }) => {
    await page.goto(BASE_URL + '/sx/', { waitUntil: 'domcontentloaded' });
    await waitForSxReady(page);
    const stepperComments = await countComments(page, "[data-sx-island='home/stepper']");
    expect(stepperComments).toBe(0);
  });
});

// ─── Counter text node structure ────────────────────────────

test.describe('stepper counter hydration', () => {

  test('counter shows "0 / 16" after hydration', async ({ page }) => {
    await page.context().clearCookies();
    await page.goto(BASE_URL + '/sx/', { waitUntil: 'domcontentloaded' });
    await waitForSxReady(page);
    const text = await page.textContent("[data-sx-island='home/stepper'] span.sx-text-sm");
    expect(text).toBe('0 / 16');
  });

  test('counter has 3 text nodes: value, separator, total', async ({ page }) => {
    await page.context().clearCookies();
    await page.goto(BASE_URL + '/sx/', { waitUntil: 'domcontentloaded' });
    await waitForSxReady(page);
    const children = await describeChildren(page, "[data-sx-island='home/stepper'] span.sx-text-sm");
    expect(children).not.toBeNull();
    expect(children.length).toBe(3);
    expect(children[0].type).toBe(3); // text node
    expect(children[0].text).toBe('0');
    expect(children[1].type).toBe(3);
    expect(children[1].text).toBe(' / ');
    expect(children[2].type).toBe(3);
    expect(children[2].text).toBe('16');
  });

  test('counter updates on forward click', async ({ page }) => {
    await page.context().clearCookies();
    await page.goto(BASE_URL + '/sx/', { waitUntil: 'domcontentloaded' });
    await waitForSxReady(page);
    await page.click("button:has-text('▶')");
    await page.waitForTimeout(300);
    const text = await page.textContent("[data-sx-island='home/stepper'] span.sx-text-sm");
    expect(text).toBe('1 / 16');
  });

  test('counter updates on back click', async ({ page }) => {
    await page.context().clearCookies();
    await page.goto(BASE_URL + '/sx/', { waitUntil: 'domcontentloaded' });
    await waitForSxReady(page);
    // Advance twice, then back once
    await page.click("button:has-text('▶')");
    await page.waitForTimeout(200);
    await page.click("button:has-text('▶')");
    await page.waitForTimeout(200);
    await page.click("button:has-text('◀')");
    await page.waitForTimeout(300);
    const text = await page.textContent("[data-sx-island='home/stepper'] span.sx-text-sm");
    expect(text).toBe('1 / 16');
  });
});

// ─── Event listener attachment ──────────────────────────────

test.describe('event listeners on hydrated elements', () => {

  test('stepper buttons have click listeners', async ({ page }) => {
    await page.goto(BASE_URL + '/sx/', { waitUntil: 'domcontentloaded' });
    await waitForSxReady(page);
    // Verify both buttons respond to clicks
    const before = await page.textContent("[data-sx-island='home/stepper'] span.sx-text-sm");
    await page.click("button:has-text('▶')");
    await page.waitForTimeout(300);
    const after = await page.textContent("[data-sx-island='home/stepper'] span.sx-text-sm");
    expect(before).not.toBe(after);
  });

  test('header navigation links work after hydration', async ({ page }) => {
    await page.goto(BASE_URL + '/sx/', { waitUntil: 'domcontentloaded' });
    await waitForSxReady(page);
    // The header should have clickable links
    const links = await page.locator("[data-sx-island='layouts/header'] a[href]").count();
    expect(links).toBeGreaterThan(0);
  });
});

// ─── Code view integrity ─────────────────────────────────────

test.describe('stepper code view', () => {

  test('code view has syntax-highlighted spans after hydration', async ({ page }) => {
    await page.context().clearCookies();
    await page.goto(BASE_URL + '/sx/', { waitUntil: 'domcontentloaded' });
    await waitForSxReady(page);
    const spanCount = await page.locator("[data-sx-island='home/stepper'] [data-code-view] span").count();
    // The code view should have many spans (one per token)
    expect(spanCount).toBeGreaterThan(20);
  });

  test('code highlighting advances with stepper', async ({ page }) => {
    await page.context().clearCookies();
    await page.goto(BASE_URL + '/sx/', { waitUntil: 'domcontentloaded' });
    await waitForSxReady(page);
    // Get initial highlighted span count (bg-amber-100 = current step)
    const initialHighlighted = await page.locator("[data-code-view] span.bg-amber-100").count();
    // Click forward
    await page.click("button:has-text('▶')");
    await page.waitForTimeout(500);
    const afterHighlighted = await page.locator("[data-code-view] span.bg-amber-100").count();
    // Highlighting should change
    expect(afterHighlighted).not.toBe(initialHighlighted);
  });
});

// ─── SSR/hydrated DOM comparison ─────────────────────────────

test.describe('SSR DOM preservation', () => {

  test('island element count unchanged after hydration', async ({ page }) => {
    // Count elements before JS runs
    await page.addInitScript(() => {
      document.addEventListener('DOMContentLoaded', () => {
        const stepper = document.querySelector("[data-sx-island='home/stepper']");
        if (stepper) {
          window.__ssrElementCount = stepper.querySelectorAll('*').length;
        }
      });
    });
    await page.goto(BASE_URL + '/sx/', { waitUntil: 'domcontentloaded' });
    await waitForSxReady(page);
    const ssrCount = await page.evaluate(() => window.__ssrElementCount);
    const hydratedCount = await page.evaluate(() => {
      const s = document.querySelector("[data-sx-island='home/stepper']");
      return s ? s.querySelectorAll('*').length : 0;
    });
    // Hydrated count should be close to SSR count (may have small additions for new nodes)
    // But should NOT be doubled (which would indicate replaceChildren created new DOM)
    expect(hydratedCount).toBeGreaterThanOrEqual(ssrCount * 0.8);
    expect(hydratedCount).toBeLessThanOrEqual(ssrCount * 1.3);
  });

  test('stepper buttons are the same DOM nodes after hydration', async ({ page }) => {
    // Tag buttons with a marker before hydration, verify they survive
    await page.addInitScript(() => {
      document.addEventListener('DOMContentLoaded', () => {
        const buttons = document.querySelectorAll("[data-sx-island='home/stepper'] button");
        buttons.forEach((b, i) => { b.__ssrMarker = 'btn-' + i; });
      });
    });
    await page.goto(BASE_URL + '/sx/', { waitUntil: 'domcontentloaded' });
    await waitForSxReady(page);
    const markers = await page.evaluate(() => {
      const buttons = document.querySelectorAll("[data-sx-island='home/stepper'] button");
      return Array.from(buttons).map(b => b.__ssrMarker);
    });
    // If DOM was preserved, markers survive. If replaceChildren ran, they're gone.
    expect(markers).toEqual(['btn-0', 'btn-1']);
  });

  test('header island elements are the same DOM nodes after hydration', async ({ page }) => {
    await page.addInitScript(() => {
      document.addEventListener('DOMContentLoaded', () => {
        const header = document.querySelector("[data-sx-island='layouts/header']");
        if (header) header.__ssrMarker = 'header-original';
        const firstDiv = header && header.querySelector('div');
        if (firstDiv) firstDiv.__ssrMarker = 'div-original';
      });
    });
    await page.goto(BASE_URL + '/sx/', { waitUntil: 'domcontentloaded' });
    await waitForSxReady(page);
    const markers = await page.evaluate(() => {
      const header = document.querySelector("[data-sx-island='layouts/header']");
      const firstDiv = header && header.querySelector('div');
      return {
        header: header ? header.__ssrMarker : null,
        div: firstDiv ? firstDiv.__ssrMarker : null
      };
    });
    expect(markers.header).toBe('header-original');
    expect(markers.div).toBe('div-original');
  });
});
