/**
 * INFRA-F41 Task 1 — extract pipeline emits per-child visual props
 *
 * Verifies that after re-extraction, every child node object in the
 * tokenized JSON carries the new visual paint fields:
 *   fills, strokes, opacity, r
 *
 * The test reads the committed tokenized sample for select_box_filled
 * (the reference component). Initially FAILS (committed file is
 * geometry-only). Passes after the nodeGeometry change + re-extract.
 */

import { readFileSync } from 'fs'
import { resolve } from 'path'
import { describe, expect, it } from 'vitest'

const TOKENIZED_PATH = resolve(
  __dirname,
  '../figma-data/normalized/components-tokenized/select_box_filled__1436_32816.json',
)

type PaintEntry = {
  type: string
  visible: boolean
  opacity: number
  blendMode: string | null
  hex?: string
  v?: string | null
}

type ChildNode = {
  id: string
  name: string
  type: string
  visible: boolean
  w: number | null
  h: number | null
  x: number | null
  y: number | null
  fills?: PaintEntry[]
  strokes?: PaintEntry[]
  opacity?: number
  r?: number | null
  characters?: string | null
  componentId?: string | null
  children?: ChildNode[]
}

type Variant = {
  id: string
  children: ChildNode[]
}

type ComponentData = {
  variants: Variant[]
}

function collectAllChildren(node: ChildNode): ChildNode[] {
  const result: ChildNode[] = [node]
  if (Array.isArray(node.children)) {
    for (const child of node.children) {
      result.push(...collectAllChildren(child))
    }
  }
  return result
}

describe('extract child visual props (INFRA-F41 Task 1)', () => {
  let data: ComponentData

  it('reads the tokenized JSON without error', () => {
    const raw = readFileSync(TOKENIZED_PATH, 'utf8')
    data = JSON.parse(raw)
    expect(data).toBeDefined()
    expect(Array.isArray(data.variants)).toBe(true)
    expect(data.variants.length).toBeGreaterThan(0)
  })

  it('every variant has a non-empty children array', () => {
    const raw = readFileSync(TOKENIZED_PATH, 'utf8')
    data = JSON.parse(raw)
    for (const variant of data.variants) {
      expect(Array.isArray(variant.children), `variant ${variant.id} should have children array`).toBe(true)
    }
  })

  it('at least one child node carries a fills array (new field)', () => {
    const raw = readFileSync(TOKENIZED_PATH, 'utf8')
    data = JSON.parse(raw)

    const allChildren: ChildNode[] = []
    for (const variant of data.variants) {
      for (const child of variant.children ?? []) {
        allChildren.push(...collectAllChildren(child))
      }
    }

    expect(allChildren.length, 'should find child nodes').toBeGreaterThan(0)

    // After Task 1 re-extract: every child must have a `fills` key (array, may be empty)
    const childrenWithFills = allChildren.filter(c => 'fills' in c)
    expect(
      childrenWithFills.length,
      `Expected all ${allChildren.length} child nodes to carry a "fills" key; got ${childrenWithFills.length}. ` +
        'Re-run nodeGeometry change and re-extract.',
    ).toBe(allChildren.length)
  })

  it('at least one child node carries a strokes key (new field)', () => {
    const raw = readFileSync(TOKENIZED_PATH, 'utf8')
    data = JSON.parse(raw)

    const allChildren: ChildNode[] = []
    for (const variant of data.variants) {
      for (const child of variant.children ?? []) {
        allChildren.push(...collectAllChildren(child))
      }
    }

    const childrenWithStrokes = allChildren.filter(c => 'strokes' in c)
    expect(
      childrenWithStrokes.length,
      `Expected all ${allChildren.length} child nodes to carry a "strokes" key; got ${childrenWithStrokes.length}.`,
    ).toBe(allChildren.length)
  })

  it('at least one child node carries an opacity key (new field)', () => {
    const raw = readFileSync(TOKENIZED_PATH, 'utf8')
    data = JSON.parse(raw)

    const allChildren: ChildNode[] = []
    for (const variant of data.variants) {
      for (const child of variant.children ?? []) {
        allChildren.push(...collectAllChildren(child))
      }
    }

    const childrenWithOpacity = allChildren.filter(c => 'opacity' in c)
    expect(
      childrenWithOpacity.length,
      `Expected all ${allChildren.length} child nodes to carry an "opacity" key; got ${childrenWithOpacity.length}.`,
    ).toBe(allChildren.length)
  })

  it('at least one child node carries an r (cornerRadius) key (new field)', () => {
    const raw = readFileSync(TOKENIZED_PATH, 'utf8')
    data = JSON.parse(raw)

    const allChildren: ChildNode[] = []
    for (const variant of data.variants) {
      for (const child of variant.children ?? []) {
        allChildren.push(...collectAllChildren(child))
      }
    }

    const childrenWithR = allChildren.filter(c => 'r' in c)
    expect(
      childrenWithR.length,
      `Expected all ${allChildren.length} child nodes to carry an "r" key; got ${childrenWithR.length}.`,
    ).toBe(allChildren.length)
  })

  it('fills arrays on child nodes contain valid paint objects (type, visible, opacity)', () => {
    const raw = readFileSync(TOKENIZED_PATH, 'utf8')
    data = JSON.parse(raw)

    const allChildren: ChildNode[] = []
    for (const variant of data.variants) {
      for (const child of variant.children ?? []) {
        allChildren.push(...collectAllChildren(child))
      }
    }

    // Only inspect children that have non-empty fills
    const childrenWithNonEmptyFills = allChildren.filter(c => Array.isArray(c.fills) && c.fills.length > 0)

    // We expect at least some children to have fills after re-extract
    // (the root frame is filled, so child frames typically inherit or have their own fills)
    for (const child of childrenWithNonEmptyFills) {
      for (const paint of child.fills!) {
        expect(paint).toHaveProperty('type')
        expect(paint).toHaveProperty('visible')
        expect(paint).toHaveProperty('opacity')
      }
    }

    // This assertion ensures we found at least some filled children after re-extract
    // (so the test isn't vacuously true with 0 non-empty fills)
    expect(
      childrenWithNonEmptyFills.length,
      'Expected at least one child node to have a non-empty fills array after re-extract',
    ).toBeGreaterThan(0)
  })
})

// ---------------------------------------------------------------------------
// INFRA-F41 Task 1.5 — INSTANCE glyph paint lift
//
// Icon INSTANCE nodes have empty own fills[]; the real glyph color lives on
// a VECTOR descendant inside the instance. The extract pipeline must DFS into
// the instance's raw children and lift the first visible SOLID fill up as the
// instance's fills so downstream verifiers can read icon colors uniformly.
// ---------------------------------------------------------------------------

describe('extract INSTANCE glyph paint lift (INFRA-F41 Task 1.5)', () => {
  const tokenized = JSON.parse(readFileSync(TOKENIZED_PATH, 'utf8'))

  /** Recursively find a node by id across variants→children trees */
  function findNodeById(root: ChildNode | ComponentData, id: string): ChildNode | null {
    if ('variants' in root) {
      for (const variant of (root as ComponentData).variants) {
        const found = findNodeById(variant as unknown as ChildNode, id)
        if (found) return found
      }
      return null
    }
    const node = root as ChildNode
    if (node.id === id) return node
    for (const child of node.children ?? []) {
      const found = findNodeById(child, id)
      if (found) return found
    }
    return null
  }

  it('chip-close INSTANCE 1436:36578 has non-empty fills after paint-lift', () => {
    const node = findNodeById(tokenized, '1436:36578')
    expect(node, 'node 1436:36578 must exist in tokenized JSON').not.toBeNull()
    expect(
      Array.isArray(node!.fills) && node!.fills.length > 0,
      `Expected INSTANCE 1436:36578 to have lifted glyph paint; got fills=${JSON.stringify(node!.fills)}`,
    ).toBe(true)
  })

  it('chip-close INSTANCE 1436:36578 glyph fill resolves to #9e9e9e (grey-6)', () => {
    const node = findNodeById(tokenized, '1436:36578')
    expect(node, 'node 1436:36578 must exist').not.toBeNull()
    const fills = node!.fills ?? []
    const solidFill = fills.find(f => f.type === 'SOLID')
    expect(solidFill, 'Expected a SOLID fill entry').not.toBeUndefined()
    expect(solidFill!.hex?.toLowerCase()).toBe('#9e9e9e')
  })

  it('chip-close INSTANCE 1436:36581 also gets paint-lift (second chip)', () => {
    // 1436:36581 is the second "icon/Edit/Clear 2" instance (Option 2 chip)
    const node = findNodeById(tokenized, '1436:36581')
    // This node may not exist in all variants; skip if absent
    if (!node) return
    expect(Array.isArray(node.fills) && node.fills.length > 0).toBe(true)
  })

  it('variant root 1436:36574 fill stays #ffffff (FRAME own fill, not overwritten)', () => {
    const variant = (tokenized as ComponentData).variants.find(v => v.id === '1436:36574')
    expect(variant, 'variant 1436:36574 must exist').not.toBeUndefined()
    const fills = (variant as unknown as ChildNode).fills ?? []
    const solidFill = fills.find((f: PaintEntry) => f.type === 'SOLID')
    expect(solidFill?.hex?.toLowerCase()).toBe('#ffffff')
  })

  it('chip frame 1436:36576 fill stays #f0f0f0 (FRAME own fill, not overwritten)', () => {
    const node = findNodeById(tokenized, '1436:36576')
    expect(node, 'node 1436:36576 must exist').not.toBeNull()
    const solidFill = (node!.fills ?? []).find(f => f.type === 'SOLID')
    expect(solidFill?.hex?.toLowerCase()).toBe('#f0f0f0')
  })
})
