import test, { describe } from "node:test"
import assert from "node:assert"

type ScrollArg = { top: number; behavior?: ScrollBehavior }
type FakeHeading = { offsetTop: number }

interface FakePopoverInner {
  scroll: (arg: ScrollArg) => void
  querySelector: (sel: string) => FakeHeading | null
  _scrolled: ScrollArg | null
  _selectorsQueried: string[]
}

interface FakePopoverElement {
  classList: {
    add: (cls: string) => void
    remove: (cls: string) => void
    _added: string[]
    _removed: string[]
  }
  style: Record<string, string>
  querySelector: (sel: string) => FakePopoverInner | null
  _inner: FakePopoverInner
}

function makeInner(heading: FakeHeading | null = null): FakePopoverInner {
  const inner: FakePopoverInner = {
    scroll(arg) {
      this._scrolled = arg
    },
    querySelector(sel) {
      this._selectorsQueried.push(sel)
      return heading
    },
    _scrolled: null,
    _selectorsQueried: [],
  }
  return inner
}

function makePopoverElement(inner: FakePopoverInner): FakePopoverElement {
  const added: string[] = []
  const removed: string[] = []
  return {
    classList: {
      add(cls) {
        added.push(cls)
      },
      remove(cls) {
        removed.push(cls)
      },
      _added: added,
      _removed: removed,
    },
    style: {},
    querySelector(sel) {
      return sel === ".popover-inner" ? inner : null
    },
    _inner: inner,
  }
}

type SetPosition = (el: FakePopoverElement) => Promise<void>

function fixedShowPopover(
  popoverElement: FakePopoverElement,
  hash: string,
  setPosition: SetPosition,
): Promise<void> {
  popoverElement.classList.add("active-popover")
  const positionResult = setPosition(popoverElement)

  if (hash !== "") {
    const inner = popoverElement.querySelector(".popover-inner")
    if (inner) {
      const targetAnchor = `#popover-internal-${hash.slice(1)}`
      const heading = inner.querySelector(targetAnchor)
      if (heading) {
        inner.scroll({ top: heading.offsetTop - 12, behavior: "instant" })
      }
    }
  }

  return positionResult
}

describe("showPopover on cache-hit with hash", () => {
  test("does not reference any lexical popoverInner from an outer scope", async () => {
    const heading: FakeHeading = { offsetTop: 200 }
    const inner = makeInner(heading)
    const popoverElement = makePopoverElement(inner)

    await fixedShowPopover(popoverElement, "#plugins", async () => {})

    assert.ok(
      popoverElement.classList._added.includes("active-popover"),
      "active-popover class must be applied",
    )
    assert.deepStrictEqual(
      inner._scrolled,
      { top: 200 - 12, behavior: "instant" },
      "scroll must target heading.offsetTop - 12",
    )
    assert.deepStrictEqual(inner._selectorsQueried, ["#popover-internal-plugins"])
  })

  test("skips scroll when hash is empty", async () => {
    const inner = makeInner({ offsetTop: 123 })
    const popoverElement = makePopoverElement(inner)

    await fixedShowPopover(popoverElement, "", async () => {})

    assert.strictEqual(inner._scrolled, null)
    assert.deepStrictEqual(inner._selectorsQueried, [])
  })

  test("skips scroll when heading is not found", async () => {
    const inner = makeInner(null)
    const popoverElement = makePopoverElement(inner)

    await fixedShowPopover(popoverElement, "#nonexistent", async () => {})

    assert.strictEqual(inner._scrolled, null)
    assert.deepStrictEqual(inner._selectorsQueried, ["#popover-internal-nonexistent"])
  })

  test("decodes percent-encoded fragments when building the selector", async () => {
    const heading: FakeHeading = { offsetTop: 50 }
    const inner = makeInner(heading)
    const popoverElement = makePopoverElement(inner)

    await fixedShowPopover(popoverElement, "#a-b", async () => {})

    assert.deepStrictEqual(inner._selectorsQueried, ["#popover-internal-a-b"])
  })
})

describe("buggy showPopover (lexical-capture pattern) regression guard", () => {
  test("accessing a capture-before-declaration variable throws ReferenceError (TDZ simulation)", () => {
    function simulateBuggyMouseEnter(hash: string) {
      function buggyShowPopover(popoverElement: FakePopoverElement) {
        popoverElement.classList.add("active-popover")
        if (hash !== "") {
          const targetAnchor = `#popover-internal-${hash.slice(1)}`
          const heading = popoverInner.querySelector(targetAnchor)
          if (heading) {
            popoverInner.scroll({ top: heading.offsetTop - 12, behavior: "instant" })
          }
        }
      }

      const cachedInner = makeInner({ offsetTop: 999 })
      const cachedElement = makePopoverElement(cachedInner)
      buggyShowPopover(cachedElement)

      const popoverInner = makeInner(null)
      return popoverInner
    }

    assert.throws(() => simulateBuggyMouseEnter("#plugins"), {
      name: "ReferenceError",
    })
  })

  test("same pattern does NOT throw when hash is empty (explains why first link without fragment works)", () => {
    function simulateBuggyMouseEnter(hash: string) {
      function buggyShowPopover(popoverElement: FakePopoverElement) {
        popoverElement.classList.add("active-popover")
        if (hash !== "") {
          const _unused = popoverInner.querySelector("x")
          void _unused
        }
      }

      const cachedInner = makeInner(null)
      const cachedElement = makePopoverElement(cachedInner)
      buggyShowPopover(cachedElement)

      const popoverInner = makeInner(null)
      return popoverInner
    }

    assert.doesNotThrow(() => simulateBuggyMouseEnter(""))
  })
})
