import assert from "node:assert/strict"
import { describe, it } from "node:test"
import { computeGraphicLayoutStyle, resizeWithHandle } from "./docs-graphic-layout.ts"
import { normalizeImportedGraphics } from "./docs-graphic-import.ts"
import {
  DOCS_GRAPHIC_DEFAULTS,
  parseGraphicAttrs,
  computeCropApplyPatch,
  computeCropDisplayGeometry,
  computeCropImageStyle,
  computeCropReeditInitialRegion,
  computeImageContentRect,
  computeImageFitStyle,
  layoutFitForImage,
  usesCropImageFit,
  resizeCropRegion,
} from "./docs-graphic-types.ts"

describe("docs-graphic", () => {
  it("computeGraphicLayoutStyle floats the wrapper so text wraps", () => {
  const layout = computeGraphicLayoutStyle({
    ...DOCS_GRAPHIC_DEFAULTS,
    graphicType: "image",
    wrap: "square",
    floatSide: "left",
  })
  assert.equal(layout.wrapper.float, "left")
  // Wrap gap sits on the text side of the float.
  assert.ok(typeof layout.wrapper.marginInlineEnd === "string")
  })

  it("normalizeImportedGraphics preserves draw scene JSON", () => {
    const scene = '{"type":"excalidraw","elements":[{"id":"a","type":"rectangle"}]}'
    const normalized = normalizeImportedGraphics({
      type: "doc",
      content: [
        {
          type: "docsGraphic",
          attrs: {
            graphicType: "draw",
            drawScene: scene,
            src: "data:image/svg+xml;charset=utf-8,%3Csvg%3E%3C/svg%3E",
            width: 320,
            height: 240,
          },
        },
      ],
    })
    const node = (normalized.content as Array<Record<string, unknown>>)[0]
    assert.equal((node.attrs as { drawScene?: string }).drawScene, scene)
  })

  it("parseGraphicAttrs preserves draw scene JSON", () => {
    const attrs = parseGraphicAttrs({
      graphicType: "draw",
      drawScene: '{"type":"excalidraw","elements":[]}',
      src: "data:image/svg+xml;charset=utf-8,%3Csvg%3E%3C/svg%3E",
      width: 320,
      height: 240,
    })
    assert.equal(attrs.graphicType, "draw")
    assert.equal(attrs.drawScene, '{"type":"excalidraw","elements":[]}')
    assert.ok(attrs.src?.startsWith("data:image/svg+xml"))
  })

  it("computeGraphicLayoutStyle applies absolute placement", () => {
  const layout = computeGraphicLayoutStyle({
    ...DOCS_GRAPHIC_DEFAULTS,
    graphicType: "shape",
    placement: "absolute",
    x: 40,
    y: 20,
  })
  assert.equal(layout.inner.position, "absolute")
  assert.equal(layout.inner.left, 40)
  assert.equal(layout.inner.top, 20)
  })

  it("resizeWithHandle respects minimum size", () => {
    const next = resizeWithHandle("se", 120, 80, -200, -200)
    assert.equal(next.width, 24)
    assert.equal(next.height, 24)
  })

  it("resizeWithHandle respects aspect lock", () => {
    const next = resizeWithHandle("se", 200, 100, 100, 50, 24, true)
    assert.equal(next.width, 300)
    assert.equal(next.height, 150)
  })

  it("normalizeImportedGraphics upgrades standalone image paragraph", () => {
  const result = normalizeImportedGraphics({
    type: "doc",
    content: [
      {
        type: "paragraph",
        content: [{ type: "image", attrs: { src: "data:image/png;base64,abc", width: 200 } }],
      },
    ],
  })
  const first = (result.content as Array<Record<string, unknown>>)[0]
  assert.equal(first.type, "docsGraphic")
  assert.equal((first.attrs as { graphicType?: string }).graphicType, "image")
  })

  it("computeGraphicLayoutStyle applies wrap margin from mm", () => {
    const layout = computeGraphicLayoutStyle({
      ...DOCS_GRAPHIC_DEFAULTS,
      graphicType: "image",
      wrap: "square",
      floatSide: "left",
      wrapMarginMm: 6,
    })
    assert.ok(typeof layout.wrapper.marginInlineEnd === "string")
    assert.notEqual(layout.wrapper.marginInlineEnd, "12px")
  })

  it("computeGraphicLayoutStyle centers float side", () => {
    const layout = computeGraphicLayoutStyle({
      ...DOCS_GRAPHIC_DEFAULTS,
      wrap: "square",
      floatSide: "center",
    })
    assert.equal(layout.wrapper.float, "none")
    assert.equal(layout.wrapper.marginInline, "auto")
  })

  it("computeGraphicLayoutStyle places fixed graphics with page coords", () => {
    const layout = computeGraphicLayoutStyle(
      {
        ...DOCS_GRAPHIC_DEFAULTS,
        positionMode: "fixed-on-page" as const,
        placement: "absolute" as const,
        pageIndex: 1,
        pageX: 30,
        pageY: 40,
      },
      { pageHeight: 1000 }
    )
    assert.equal(layout.usePageLayer, true)
    assert.equal(layout.inner.left, 30)
    assert.ok(typeof layout.inner.top === "number" && layout.inner.top > 1000)
  })

  it("parseGraphicAttrs forces fixed-on-page for absolute placement", () => {
    const attrs = parseGraphicAttrs({
      ...DOCS_GRAPHIC_DEFAULTS,
      placement: "absolute",
      positionMode: "move-with-text",
    })
    assert.equal(attrs.positionMode, "fixed-on-page")
  })

  it("parseGraphicAttrs migrates legacy absolute to fixed-on-page", () => {
    const attrs = parseGraphicAttrs({
      ...DOCS_GRAPHIC_DEFAULTS,
      placement: "absolute",
      wrap: "behind",
      x: 20,
      y: 30,
    })
    assert.equal(attrs.positionMode, "fixed-on-page")
    assert.equal(attrs.pageX, 20)
    assert.equal(attrs.pageY, 30)
  })

  it("normalizeImportedGraphics maps docx wrap aliases", () => {
  const result = normalizeImportedGraphics({
    type: "doc",
    content: [
      {
        type: "docsGraphic",
        attrs: {
          graphicType: "image",
          src: "https://example.com/a.png",
          wrap: "topAndBottom",
          placement: "anchored",
        },
      },
    ],
  })
  const attrs = (result.content as Array<Record<string, unknown>>)[0].attrs as {
    wrap?: string
    placement?: string
  }
  assert.equal(attrs.wrap, "top-bottom")
  assert.equal(attrs.placement, "absolute")
  })

  it("computeImageContentRect letterboxes wide images", () => {
    const rect = computeImageContentRect(200, 100, 400, 200)
    assert.equal(rect.width, 200)
    assert.equal(rect.height, 100)
    assert.equal(rect.top, 0)
  })

  it("computeImageContentRect letterboxes tall images", () => {
    const rect = computeImageContentRect(200, 100, 200, 400)
    assert.equal(rect.height, 100)
    assert.equal(rect.width, 50)
    assert.equal(rect.left, 75)
  })

  it("computeImageContentRect cover fills the frame", () => {
    const rect = computeImageContentRect(200, 100, 200, 400, "cover")
    assert.equal(rect.width, 200)
    assert.equal(rect.height, 400)
    assert.equal(rect.top, -150)
  })

  it("computeImageContentRect cover anchor shifts focal point", () => {
    const centered = computeImageContentRect(200, 100, 400, 200, "cover", 0.5, 0.5)
    const left = computeImageContentRect(200, 100, 400, 200, "cover", 0, 0.5)
    assert.ok(left.left > centered.left)
  })

  it("parseGraphicAttrs accepts crop imageFit", () => {
    const attrs = parseGraphicAttrs({ ...DOCS_GRAPHIC_DEFAULTS, imageFit: "crop" })
    assert.equal(attrs.imageFit, "crop")
    assert.equal(usesCropImageFit(attrs.imageFit), true)
    assert.equal(layoutFitForImage(attrs.imageFit), "contain")
  })

  it("computeImageFitStyle maps anchors to object-position", () => {
    const style = computeImageFitStyle({
      imageFit: "cover",
      imageFitAnchorH: 0,
      imageFitAnchorV: 1,
    })
    assert.equal(style.objectFit, "cover")
    assert.equal(style.objectPosition, "0% 100%")
  })

  it("resizeCropRegion shrinks from east without moving origin", () => {
    const next = resizeCropRegion("e", { cropX: 0.1, cropY: 0, cropWidth: 0.8, cropHeight: 1 }, -0.2, 0)
    assert.equal(next.cropX, 0.1)
    assert.equal(next.cropWidth, 0.6)
  })

  it("resizeCropRegion shrinks from west and shifts cropX", () => {
    const next = resizeCropRegion("w", { cropX: 0.1, cropY: 0, cropWidth: 0.8, cropHeight: 1 }, 0.2, 0)
    assert.equal(next.cropX, 0.3)
    assert.equal(next.cropWidth, 0.6)
  })

  it("computeCropImageStyle maps source crop 1:1 to frame", () => {
    const style = computeCropImageStyle(
      {
        cropX: 0,
        cropY: 0,
        cropWidth: 0.5,
        cropHeight: 1,
        cropShape: "rect",
      },
      100,
      100,
      400,
      200
    )
    assert.equal(style.img.width, 200)
    assert.equal(style.img.height, 100)
    assert.equal(Math.abs(style.img.left as number), 0)
    assert.equal(style.img.objectFit, undefined)
  })

  it("computeCropImageStyle offsets the image by the crop origin", () => {
    const style = computeCropImageStyle(
      {
        cropX: 0.25,
        cropY: 0.1,
        cropWidth: 0.5,
        cropHeight: 0.5,
        cropShape: "rect",
      },
      100,
      100,
      400,
      400
    )
    assert.equal(style.img.position, "absolute")
    assert.equal(style.img.left, -50)
    assert.equal(style.img.top, -20)
  })

  it("computeCropDisplayGeometry re-edit uses full image as crop window", () => {
    const baseSource = { cropX: 0, cropY: 0, cropWidth: 0.5, cropHeight: 1 }
    const initial = computeCropReeditInitialRegion(baseSource, 100, 100, 400, 200)
    const geometry = computeCropDisplayGeometry(
      { ...DOCS_GRAPHIC_DEFAULTS, ...initial, imageFit: "crop" },
      100,
      100,
      400,
      200,
      baseSource
    )
    assert.equal(geometry.windowRect.width, 200)
    assert.equal(geometry.windowRect.height, 100)
    assert.equal(geometry.cropRect.width, 100)
    assert.equal(geometry.imageRect.width, 200)
    assert.equal(initial.cropWidth, 0.5)
  })

  it("computeCropApplyPatch re-edit can expand crop to full source", () => {
    const baseSource = { cropX: 0, cropY: 0, cropWidth: 0.5, cropHeight: 1 }
    const patch = computeCropApplyPatch(
      {
        ...DOCS_GRAPHIC_DEFAULTS,
        cropX: 0,
        cropY: 0,
        cropWidth: 1,
        cropHeight: 1,
        imageFit: "crop",
        width: 100,
        height: 100,
      },
      0,
      0,
      400,
      200,
      baseSource
    )
    assert.equal(patch.cropWidth, 1)
    assert.equal(patch.width, 200)
    assert.equal(patch.height, 100)
  })

  it("computeCropApplyPatch resizes frame to cropped section", () => {
    const patch = computeCropApplyPatch(
      {
        ...DOCS_GRAPHIC_DEFAULTS,
        cropX: 0,
        cropY: 0,
        cropWidth: 0.5,
        cropHeight: 1,
        imageFit: "contain",
        width: 200,
        height: 100,
      },
      0,
      0,
      400,
      200
    )
    assert.equal(patch.width, 100)
    assert.equal(patch.height, 100)
    assert.equal(patch.cropWidth, 0.5)
    assert.equal(patch.imageFit, "crop")
  })
})
