import { beforeEach, describe, expect, it } from "vitest"
import {
  __resetIdCounter,
  convertV1ToV2,
  convertV2ToV1,
  isSnippetToken,
  snippetKeyFromToken,
  snippetToken,
  v1CardToV2,
  v1CardsToV2,
  v1TabsToV2,
  v2CardToV1,
} from "@/adapters"
import type { V1CardConfig } from "@/adapters"
import type { DisplayCard } from "@/definitions/cards"
import type { CardElement } from "@/definitions/elements"

beforeEach(() => {
  __resetIdCounter()
})

describe("snippet token helpers", () => {
  it("round-trips key ⇄ token", () => {
    expect(snippetToken("finish-card")).toBe("snippet#finish-card")
    expect(isSnippetToken("snippet#finish-card")).toBe(true)
    expect(isSnippetToken("card-1")).toBe(false)
    expect(snippetKeyFromToken("snippet#finish-card")).toBe("finish-card")
  })
})

describe("v1 → v2 snippet placements", () => {
  it("turns a no-override snippet node into an instance token in elementList", () => {
    const bundle = v1CardToV2({
      type: "fields_list",
      fieldsList: [
        { type: "text", name: "a", label: "A" },
        { type: "snippet", key: "addr" } as never,
        { type: "email", name: "b", label: "B" },
      ],
    })
    const card = bundle.cards[bundle.ids[0]!] as unknown as Record<string, unknown>
    const list = card.elementList as string[]
    expect(list).toHaveLength(3)
    // Every placement carries a unique instance id (snippet#<key>:<id>).
    expect(list[1]).toMatch(/^snippet#addr:/)
    // No backing element is created for the token.
    expect(Object.keys(bundle.elements)).toHaveLength(2)
    // No overrides → no entries in the overrides map.
    expect(Object.keys(bundle.snippetOverrides)).toHaveLength(0)
  })

  it("registers per-use overrides under an instance id token", () => {
    const bundle = v1CardToV2({
      type: "fields_list",
      fieldsList: [
        { type: "snippet", key: "addr", title: "Home", required: true } as never,
      ],
    })
    const card = bundle.cards[bundle.ids[0]!] as unknown as Record<string, unknown>
    const token = (card.elementList as string[])[0]!
    // Token is snippet#<key>:<instanceId>.
    expect(token.startsWith("snippet#addr:")).toBe(true)
    const instanceId = token.slice("snippet#addr:".length)
    expect(bundle.snippetOverrides[instanceId]).toEqual({
      title: "Home",
      required: true,
    })
  })

  it("keeps two placements of one key with different overrides distinct", () => {
    const bundle = v1CardToV2({
      type: "fields_list",
      fieldsList: [
        { type: "snippet", key: "addr", title: "Home" } as never,
        { type: "snippet", key: "addr", title: "Work" } as never,
      ],
    })
    const card = bundle.cards[bundle.ids[0]!] as unknown as Record<string, unknown>
    const [t1, t2] = card.elementList as string[]
    expect(t1).not.toBe(t2)
    const overrides = Object.values(bundle.snippetOverrides)
    expect(overrides).toHaveLength(2)
    expect(overrides).toContainEqual({ title: "Home" })
    expect(overrides).toContainEqual({ title: "Work" })
  })

  it("turns a snippet card node into a token in cardList", () => {
    const bundle = v1CardsToV2([
      { type: "card_list", cards: [{ type: "snippet", key: "finish" } as never] },
    ])
    const root = bundle.cards[bundle.ids[0]!] as unknown as Record<string, unknown>
    expect((root.cardList as string[])[0]).toMatch(/^snippet#finish:/)
  })

  it("handles a snippet inside a tab's body", () => {
    const bundle = v1TabsToV2([
      {
        title: "T",
        tabId: "t",
        cards: [
          { type: "card_list", cards: [{ type: "snippet", key: "s1" } as never] },
        ],
      },
    ] as unknown as V1CardConfig[] as never)
    const tabCard = bundle.cards["t"] as unknown as Record<string, unknown>
    // The tab is a card-list; its inner card-list card carries the token.
    const innerId = (tabCard.cardList as string[])[0]!
    const inner = bundle.cards[innerId] as unknown as Record<string, unknown>
    expect((inner.cardList as string[])[0]).toMatch(/^snippet#s1:/)
  })
})

describe("v2 → v1 snippet placements", () => {
  const cards = {
    c1: {
      id: "c1",
      cardType: "element-list",
      title: "Card",
      elementList: ["e1", "snippet#addr", "e2"],
    },
  } as unknown as Record<string, DisplayCard>
  const elements = {
    e1: { id: "e1", type: "input.text", name: "a", label: "A" },
    e2: { id: "e2", type: "input.email", name: "b", label: "B" },
  } as unknown as Record<string, CardElement>

  it("decodes a token in elementList back to a v1 snippet node", () => {
    const v1 = v2CardToV1("c1", cards, elements)!
    expect(v1.fieldsList).toHaveLength(3)
    expect(v1.fieldsList?.[1]).toEqual({ type: "snippet", key: "addr" })
    // Surrounding fields convert normally.
    expect(v1.fieldsList?.[0]?.type).toBe("text")
    expect(v1.fieldsList?.[2]?.type).toBe("email")
  })

  it("decodes a token in cardList back to a v1 snippet node", () => {
    const cl = {
      root: { id: "root", cardType: "card-list", cardList: ["snippet#finish"] },
    } as unknown as Record<string, DisplayCard>
    const v1 = v2CardToV1("root", cl, {})!
    expect(v1.cards?.[0]).toEqual({ type: "snippet", key: "finish" })
  })

  it("merges per-use overrides back onto the decoded node", () => {
    const cardsWithOv = {
      c1: {
        id: "c1",
        cardType: "element-list",
        elementList: ["snippet#addr:ov1"],
      },
    } as unknown as Record<string, DisplayCard>
    const v1 = v2CardToV1("c1", cardsWithOv, {}, {
      ov1: { title: "Home", required: true },
    })!
    expect(v1.fieldsList?.[0]).toEqual({
      id: "ov1",
      type: "snippet",
      key: "addr",
      title: "Home",
      required: true,
    })
  })
})

describe("card.tab / card.list container elements", () => {
  it("converts a card.tab element to a v1 tabs_list with inlined children", () => {
    const cards = {
      root: {
        id: "root",
        cardType: "element-list",
        elementList: ["tabEl"],
      },
    } as unknown as Record<string, DisplayCard>
    const elements = {
      tabEl: {
        id: "tabEl",
        type: "card.tab",
        title: "Tabs",
        tabs: [
          { id: "t1", label: "One", elementList: ["f1"] },
          { id: "t2", label: "Two", elementList: [] },
        ],
      },
      f1: { id: "f1", type: "input.text", name: "x", label: "X" },
    } as unknown as Record<string, CardElement>

    const v1 = v2CardToV1("root", cards, elements)!
    const node = v1.fieldsList?.[0] as Record<string, unknown>
    expect(node.type).toBe("tabs_list")
    const tabsList = node.tabsList as Array<Record<string, unknown>>
    expect(tabsList).toHaveLength(2)
    expect(tabsList[0]!.title).toBe("One")
    expect((tabsList[0]!.fieldsList as unknown[])).toHaveLength(1)
    expect(((tabsList[0]!.fieldsList as Array<Record<string, unknown>>)[0]!).type).toBe(
      "text",
    )
  })

  it("round-trips a card.tab element v2 → v1 → v2 without losing tabs", () => {
    const cards = {
      root: { id: "root", cardType: "element-list", elementList: ["tabEl"] },
    } as unknown as Record<string, DisplayCard>
    const elements = {
      tabEl: {
        id: "tabEl",
        type: "card.tab",
        title: "Tabs",
        tabs: [
          { id: "t1", label: "One", elementList: ["f1", "f2"] },
          { id: "t2", label: "Two", elementList: ["f3"] },
        ],
      },
      f1: { id: "f1", type: "input.text", name: "a", label: "A" },
      f2: { id: "f2", type: "boolean.checkbox", name: "b", label: "B" },
      f3: { id: "f3", type: "input.email", name: "c", label: "C" },
    } as unknown as Record<string, CardElement>

    const v1 = convertV2ToV1(cards.root!, cards, elements)
    const schema = convertV1ToV2(v1 as never)
    const root = schema.cards[schema.rootCardId] as unknown as Record<string, unknown>
    const tabElId = (root.elementList as string[])[0]!
    const tabEl = schema.elements[tabElId] as unknown as Record<string, unknown>
    expect(tabEl.type).toBe("card.tab")
    const tabs = tabEl.tabs as Array<Record<string, unknown>>
    expect(tabs).toHaveLength(2)
    expect(tabs[0]!.label).toBe("One")
    // The tab's children survived and resolve to real elements.
    const tab0Children = tabs[0]!.elementList as string[]
    expect(tab0Children).toHaveLength(2)
    expect(schema.elements[tab0Children[0]!]!.type).toBe("input.text")
    expect((tabs[1]!.elementList as string[])).toHaveLength(1)
  })

  it("round-trips data/grid/info card layout elements (no __unknownV1Type)", () => {
    const cards = {
      root: {
        id: "root",
        cardType: "element-list",
        elementList: ["dt", "gd", "nf"],
      },
    } as unknown as Record<string, DisplayCard>
    const elements = {
      dt: { id: "dt", type: "card.datatable", title: "T", dataRoute: "/x" },
      gd: { id: "gd", type: "card.grid", title: "G", gridCols: 4 },
      nf: { id: "nf", type: "card.info", title: "I", icon: "info" },
    } as unknown as Record<string, CardElement>

    const v1 = convertV2ToV1(cards.root!, cards, elements) as Record<string, unknown>
    const types = (v1.fieldsList as Array<Record<string, unknown>>).map((f) => f.type)
    expect(types).toEqual(["data_table", "grid_view", "info_card"])

    const schema = convertV1ToV2(v1 as never)
    const root = schema.cards[schema.rootCardId] as unknown as Record<string, unknown>
    const elTypes = (root.elementList as string[]).map(
      (id) => (schema.elements[id] as unknown as Record<string, unknown>).type,
    )
    expect(elTypes).toEqual(["card.datatable", "card.grid", "card.info"])
    // Config survives + no unknown-type fallback.
    expect(
      (schema.elements[(root.elementList as string[])[0]!] as unknown as Record<string, unknown>)
        .dataRoute,
    ).toBe("/x")
    for (const id of root.elementList as string[]) {
      expect(
        (schema.elements[id] as unknown as Record<string, unknown>).__unknownV1Type,
      ).toBeUndefined()
    }
  })

  it("round-trips a card.list element v2 → v1 → v2", () => {
    const cards = {
      root: { id: "root", cardType: "element-list", elementList: ["listEl"] },
    } as unknown as Record<string, DisplayCard>
    const elements = {
      listEl: {
        id: "listEl",
        type: "card.list",
        title: "Group",
        elementList: ["f1"],
      },
      f1: { id: "f1", type: "input.text", name: "a", label: "A" },
    } as unknown as Record<string, CardElement>

    const v1 = convertV2ToV1(cards.root!, cards, elements)
    const schema = convertV1ToV2(v1 as never)
    const root = schema.cards[schema.rootCardId] as unknown as Record<string, unknown>
    const listElId = (root.elementList as string[])[0]!
    const listEl = schema.elements[listElId] as unknown as Record<string, unknown>
    expect(listEl.type).toBe("card.list")
    expect((listEl.elementList as string[])).toHaveLength(1)
    expect(
      schema.elements[(listEl.elementList as string[])[0]!]!.type,
    ).toBe("input.text")
  })
})

describe("round-trip", () => {
  it("v1 → v2 → v1 preserves snippet placements (key only)", () => {
    const original: V1CardConfig = {
      type: "fields_list",
      title: "Form",
      fieldsList: [
        { type: "text", name: "first", label: "First" },
        { type: "snippet", key: "addr" } as never,
      ],
    }
    const bundle = v1CardToV2(original)
    const back = convertV2ToV1(
      bundle.cards[bundle.ids[0]!]!,
      bundle.cards,
      bundle.elements,
    ) as V1CardConfig
    expect(back.type).toBe("fields_list")
    // Carries the generated instance id back as the node's `id`.
    expect(back.fieldsList?.[1]).toMatchObject({ type: "snippet", key: "addr" })
    expect(typeof back.fieldsList?.[1]?.id).toBe("string")
  })

  it("convertV1ToV2 keeps a snippet token in an element-list root", () => {
    const schema = convertV1ToV2([
      { type: "text", name: "a", label: "A" },
      { type: "snippet", key: "addr" } as never,
    ])
    const root = schema.cards[schema.rootCardId] as unknown as Record<
      string,
      unknown
    >
    expect(
      (root.elementList as string[]).some((id) => id.startsWith("snippet#addr")),
    ).toBe(true)
  })

  it("round-trips per-use overrides losslessly (different values per placement)", () => {
    const original: V1CardConfig = {
      type: "fields_list",
      fieldsList: [
        { type: "snippet", key: "addr", title: "Home" } as never,
        { type: "snippet", key: "addr", title: "Work", required: true } as never,
      ],
    }
    const bundle = v1CardToV2(original)
    const back = convertV2ToV1(
      bundle.cards[bundle.ids[0]!]!,
      bundle.cards,
      bundle.elements,
      bundle.snippetOverrides,
    ) as V1CardConfig
    expect(back.fieldsList?.[0]).toMatchObject({
      type: "snippet",
      key: "addr",
      title: "Home",
    })
    expect(back.fieldsList?.[1]).toMatchObject({
      type: "snippet",
      key: "addr",
      title: "Work",
      required: true,
    })
    // Distinct instance ids keep the two placements addressable.
    expect(back.fieldsList?.[0]?.id).not.toBe(back.fieldsList?.[1]?.id)
  })

  it("round-trips an explicit instance id ({id,type,key} ⇄ snippet#key:id)", () => {
    const bundle = v1CardToV2({
      type: "fields_list",
      fieldsList: [
        { id: "inst-1", type: "snippet", key: "card-one", title: "X" } as never,
      ],
    })
    const card = bundle.cards[bundle.ids[0]!] as unknown as Record<string, unknown>
    // The node's own id becomes the token's instance id.
    expect((card.elementList as string[])[0]).toBe("snippet#card-one:inst-1")
    // The id is NOT treated as an override value.
    expect(bundle.snippetOverrides["inst-1"]).toEqual({ title: "X" })

    const back = convertV2ToV1(
      bundle.cards[bundle.ids[0]!]!,
      bundle.cards,
      bundle.elements,
      bundle.snippetOverrides,
    ) as V1CardConfig
    expect(back.fieldsList?.[0]).toEqual({
      id: "inst-1",
      type: "snippet",
      key: "card-one",
      title: "X",
    })
  })

  it("convertV1ToV2 schema → convertV2ToV1 round-trips overrides", () => {
    const schema = convertV1ToV2({
      type: "fields_list",
      fieldsList: [{ type: "snippet", key: "addr", title: "Home" } as never],
    })
    // The schema carries its own snippetOverrides; convertV2ToV1 reads them.
    expect(Object.keys(schema.snippetOverrides ?? {})).toHaveLength(1)
    const back = convertV2ToV1(schema) as V1CardConfig
    expect(back.fieldsList?.[0]).toMatchObject({
      type: "snippet",
      key: "addr",
      title: "Home",
    })
  })
})
