import { test } from 'uvu'
import { equal, is, match, throws, type } from 'uvu/assert'

import { AtRule, Declaration, parse, Root, Rule } from '../lib/postcss.js'

let example =
  'a { a: 1; b: 2 }' +
  '/* a */' +
  '@keyframes anim {' +
  '/* b */' +
  'to { c: 3 }' +
  '}' +
  '@media all and (min-width: 100) {' +
  'em { d: 4 }' +
  '@page {' +
  'e: 5;' +
  '/* c */' +
  '}' +
  '}'

test('throws error on declaration without value', () => {
  throws(() => {
    // @ts-expect-error Testing invalid API
    new Rule().append({ prop: 'color', vlaue: 'black' })
  }, /Value field is missed/)
})

test('throws error on unknown node type', () => {
  throws(() => {
    // @ts-expect-error Testing invalid API
    new Rule().append({ foo: 'bar' })
  }, /Unknown node type/)
})

test('push() adds child without checks', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  rule.push(new Declaration({ prop: 'c', value: '3' }))
  is(rule.toString(), 'a { a: 1; b: 2; c: 3 }')
  is(rule.nodes.length, 3)
  type(rule.last?.raws.before, 'undefined')
})

test('each() iterates', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  let indexes: number[] = []

  let result = rule.each((decl, i) => {
    indexes.push(i)
    is(decl, rule.nodes[i])
  })

  type(result, 'undefined')
  equal(indexes, [0, 1])
})

test('each() iterates with prepend', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  let size = 0

  rule.each(() => {
    rule.prepend({ prop: 'color', value: 'aqua' })
    size += 1
  })

  is(size, 2)
})

test('each() iterates with prepend insertBefore', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  let size = 0

  rule.each(decl => {
    if (decl.type === 'decl' && decl.prop === 'a') {
      rule.insertBefore(decl, { prop: 'c', value: '3' })
    }
    size += 1
  })

  is(size, 2)
})

test('each() iterates with append insertBefore', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  let size = 0

  rule.each((decl, i) => {
    if (decl.type === 'decl' && decl.prop === 'a') {
      rule.insertBefore(i + 1, { prop: 'c', value: '3' })
    }
    size += 1
  })

  is(size, 3)
})

test('each() iterates with prepend insertAfter', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  let size = 0

  rule.each((decl, i) => {
    rule.insertAfter(i - 1, { prop: 'c', value: '3' })
    size += 1
  })

  is(size, 2)
})

test('each() iterates with append insertAfter', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  let size = 0

  rule.each((decl, i) => {
    if (decl.type === 'decl' && decl.prop === 'a') {
      rule.insertAfter(i, { prop: 'c', value: '3' })
    }
    size += 1
  })

  is(size, 3)
})

test('each() iterates with remove', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  let size = 0

  rule.each(() => {
    rule.removeChild(0)
    size += 1
  })

  is(size, 2)
})

test('each() breaks iteration', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  let indexes: number[] = []

  let result = rule.each((decl, i) => {
    indexes.push(i)
    return false
  })

  is(result, false)
  equal(indexes, [0])
})

test('each() allows to change children', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  let props: string[] = []

  rule.each(decl => {
    if (decl.type === 'decl') {
      props.push(decl.prop)
      rule.nodes = [rule.last as any, rule.first as any]
    }
  })

  equal(props, ['a', 'a'])
})

test('walk() iterates', () => {
  let types: string[] = []
  let indexes: number[] = []

  let result = parse(example).walk((node, i) => {
    types.push(node.type)
    indexes.push(i)
  })

  type(result, 'undefined')
  equal(types, [
    'rule',
    'decl',
    'decl',
    'comment',
    'atrule',
    'comment',
    'rule',
    'decl',
    'atrule',
    'rule',
    'decl',
    'atrule',
    'decl',
    'comment'
  ])
  equal(indexes, [0, 0, 1, 1, 2, 0, 1, 0, 3, 0, 0, 1, 0, 1])
})

test('walk() breaks iteration', () => {
  let indexes: number[] = []

  let result = parse(example).walk((decl, i) => {
    indexes.push(i)
    return false
  })

  is(result, false)
  equal(indexes, [0])
})

test('walk() adds CSS position to error stack', () => {
  let error = new Error('T')
  error.stack = 'Error: T\n    at b (b.js:2:4)\n    at a (a.js:2:1)'
  let root = parse(example, { from: '/c.css' })
  let catched: any
  try {
    root.walk(() => {
      throw error
    })
  } catch (e) {
    catched = e
  }
  is(catched, error)
  is(catched.postcssNode.toString(), 'a { a: 1; b: 2 }')
  is(
    catched.stack.replace(/.:\\/i, '/'),
    'Error: T\n    at /c.css:1:1\n    at b (b.js:2:4)\n    at a (a.js:2:1)'
  )
})

test('walkDecls() iterates', () => {
  let props: string[] = []
  let indexes: number[] = []

  let result = parse(example).walkDecls((decl, i) => {
    props.push(decl.prop)
    indexes.push(i)
  })

  type(result, 'undefined')
  equal(props, ['a', 'b', 'c', 'd', 'e'])
  equal(indexes, [0, 1, 0, 0, 0])
})

test('walkDecls() iterates with changes', () => {
  let size = 0
  parse(example).walkDecls((decl, i) => {
    decl.parent?.removeChild(i)
    size += 1
  })
  is(size, 5)
})

test('walkDecls() breaks iteration', () => {
  let indexes: number[] = []

  let result = parse(example).walkDecls((decl, i) => {
    indexes.push(i)
    return false
  })

  is(result, false)
  equal(indexes, [0])
})

test('walkDecls() filters declarations by property name', () => {
  let css = parse('@page{a{one:1}}b{one:1;two:2}')
  let size = 0

  css.walkDecls('one', decl => {
    is(decl.prop, 'one')
    size += 1
  })

  is(size, 2)
})

test('walkDecls() breaks declarations filter by name', () => {
  let css = parse('@page{a{one:1}}b{one:1;two:2}')
  let size = 0

  css.walkDecls('one', () => {
    size += 1
    return false
  })

  is(size, 1)
})

test('walkDecls() filters declarations by property regexp', () => {
  let css = parse('@page{a{one:1}}b{one-x:1;two:2}')
  let size = 0

  css.walkDecls(/one(-x)?/, () => {
    size += 1
  })

  is(size, 2)
})

test('walkDecls() breaks declarations filters by regexp', () => {
  let css = parse('@page{a{one:1}}b{one-x:1;two:2}')
  let size = 0

  css.walkDecls(/one(-x)?/, () => {
    size += 1
    return false
  })

  is(size, 1)
})

test('walkComments() iterates', () => {
  let texts: string[] = []
  let indexes: number[] = []

  let result = parse(example).walkComments((comment, i) => {
    texts.push(comment.text)
    indexes.push(i)
  })

  type(result, 'undefined')
  equal(texts, ['a', 'b', 'c'])
  equal(indexes, [1, 0, 1])
})

test('walkComments() iterates with changes', () => {
  let size = 0
  parse(example).walkComments((comment, i) => {
    comment.parent?.removeChild(i)
    size += 1
  })
  is(size, 3)
})

test('walkComments() breaks iteration', () => {
  let indexes: number[] = []

  let result = parse(example).walkComments((comment, i) => {
    indexes.push(i)
    return false
  })

  is(result, false)
  equal(indexes, [1])
})

test('walkRules() iterates', () => {
  let selectors: string[] = []
  let indexes: number[] = []

  let result = parse(example).walkRules((rule, i) => {
    selectors.push(rule.selector)
    indexes.push(i)
  })

  type(result, 'undefined')
  equal(selectors, ['a', 'to', 'em'])
  equal(indexes, [0, 1, 0])
})

test('walkRules() iterates with changes', () => {
  let size = 0
  parse(example).walkRules((rule, i) => {
    rule.parent?.removeChild(i)
    size += 1
  })
  is(size, 3)
})

test('walkRules() breaks iteration', () => {
  let indexes: number[] = []

  let result = parse(example).walkRules((rule, i) => {
    indexes.push(i)
    return false
  })

  is(result, false)
  equal(indexes, [0])
})

test('walkRules() filters by selector', () => {
  let size = 0
  parse('a{}b{}a{}').walkRules('a', rule => {
    is(rule.selector, 'a')
    size += 1
  })
  is(size, 2)
})

test('walkRules() breaks selector filters', () => {
  let size = 0
  parse('a{}b{}a{}').walkRules('a', () => {
    size += 1
    return false
  })
  is(size, 1)
})

test('walkRules() filters by regexp', () => {
  let size = 0
  parse('a{}a b{}b a{}').walkRules(/^a/, rule => {
    match(rule.selector, /^a/)
    size += 1
  })
  is(size, 2)
})

test('walkRules() breaks selector regexp', () => {
  let size = 0
  parse('a{}b a{}b a{}').walkRules(/^a/, () => {
    size += 1
    return false
  })
  is(size, 1)
})

test('walkAtRules() iterates', () => {
  let names: string[] = []
  let indexes: number[] = []

  let result = parse(example).walkAtRules((atrule, i) => {
    names.push(atrule.name)
    indexes.push(i)
  })

  type(result, 'undefined')
  equal(names, ['keyframes', 'media', 'page'])
  equal(indexes, [2, 3, 1])
})

test('walkAtRules() iterates with changes', () => {
  let size = 0
  parse(example).walkAtRules((atrule, i) => {
    atrule.parent?.removeChild(i)
    size += 1
  })
  is(size, 3)
})

test('walkAtRules() breaks iteration', () => {
  let indexes: number[] = []

  let result = parse(example).walkAtRules((atrule, i) => {
    indexes.push(i)
    return false
  })

  is(result, false)
  equal(indexes, [2])
})

test('walkAtRules() filters at-rules by name', () => {
  let css = parse('@page{@page 2{}}@media print{@page{}}')
  let size = 0

  css.walkAtRules('page', atrule => {
    is(atrule.name, 'page')
    size += 1
  })

  is(size, 3)
})

test('walkAtRules() breaks name filter', () => {
  let size = 0
  parse('@page{@page{@page{}}}').walkAtRules('page', () => {
    size += 1
    return false
  })
  is(size, 1)
})

test('walkAtRules() filters at-rules by name regexp', () => {
  let css = parse('@page{@page 2{}}@media print{@pages{}}')
  let size = 0

  css.walkAtRules(/page/, () => {
    size += 1
  })

  is(size, 3)
})

test('walkAtRules() breaks regexp filter', () => {
  let size = 0
  parse('@page{@pages{@page{}}}').walkAtRules(/page/, () => {
    size += 1
    return false
  })
  is(size, 1)
})

test('append() appends child', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  rule.append({ prop: 'c', value: '3' })
  is(rule.toString(), 'a { a: 1; b: 2; c: 3 }')
  is(rule.last?.raws.before, ' ')
})

test('append() appends multiple children', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  rule.append({ prop: 'c', value: '3' }, { prop: 'd', value: '4' })
  is(rule.toString(), 'a { a: 1; b: 2; c: 3; d: 4 }')
  is(rule.last?.raws.before, ' ')
})

test('append() has declaration shortcut', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  rule.append({ prop: 'c', value: '3' })
  is(rule.toString(), 'a { a: 1; b: 2; c: 3 }')
})

test('append() has rule shortcut', () => {
  let root = new Root()
  root.append({ selector: 'a' })
  is(root.first?.toString(), 'a {}')
})

test('append() has at-rule shortcut', () => {
  let root = new Root()
  root.append({ name: 'encoding', params: '"utf-8"' })
  is(root.first?.toString(), '@encoding "utf-8"')
})

test('append() has comment shortcut', () => {
  let root = new Root()
  root.append({ text: 'ok' })
  is(root.first?.toString(), '/* ok */')
})

test('append() receives root', () => {
  let css = parse('a {}')
  css.append(parse('b {}'))
  is(css.toString(), 'a {}b {}')
})

test('append() reveives string', () => {
  let root = new Root()
  root.append('a{}b{}')
  let a = root.first as Rule
  a.append('color:black')
  is(root.toString(), 'a{color:black}b{}')
  type(a.first?.source, 'undefined')
})

test('append() receives array', () => {
  let a = parse('a{ z-index: 1 }')
  let b = parse('b{ width: 1px; height: 2px }')
  let aRule = a.first as Rule
  let bRule = b.first as Rule

  aRule.append(bRule.nodes)
  is(a.toString(), 'a{ z-index: 1; width: 1px; height: 2px }')
  is(b.toString(), 'b{ }')
})

test('append() move node on insert', () => {
  let a = parse('a{}')
  let b = parse('b{}')

  b.append(a.first)
  let bLast = b.last as Rule
  bLast.selector = 'b a'

  is(a.toString(), '')
  is(b.toString(), 'b{}b a{}')
})

test('prepend() prepends child', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  rule.prepend({ prop: 'c', value: '3' })
  is(rule.toString(), 'a { c: 3; a: 1; b: 2 }')
  is(rule.first?.raws.before, ' ')
})

test('prepend() prepends multiple children', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  rule.prepend({ prop: 'c', value: '3' }, { prop: 'd', value: '4' })
  is(rule.toString(), 'a { c: 3; d: 4; a: 1; b: 2 }')
  is(rule.first?.raws.before, ' ')
})

test('prepend() receive hash instead of declaration', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  rule.prepend({ prop: 'c', value: '3' })
  is(rule.toString(), 'a { c: 3; a: 1; b: 2 }')
})

test('prepend() receives root', () => {
  let css = parse('a {}')
  css.prepend(parse('b {}'))
  is(css.toString(), 'b {}\na {}')
})

test('prepend() receives string', () => {
  let css = parse('a {}')
  css.prepend('b {}')
  is(css.toString(), 'b {}\na {}')
})

test('prepend() receives array', () => {
  let a = parse('a{ z-index: 1 }')
  let b = parse('b{ width: 1px; height: 2px }')
  let aRule = a.first as Rule
  let bRule = b.first as Rule

  aRule.prepend(bRule.nodes)
  is(a.toString(), 'a{ width: 1px; height: 2px; z-index: 1 }')
})

test('prepend() works on empty container', () => {
  let root = parse('')
  root.prepend(new Rule({ selector: 'a' }))
  is(root.toString(), 'a {}')
})

test('insertBefore() inserts child', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  rule.insertBefore(1, { prop: 'c', value: '3' })
  is(rule.toString(), 'a { a: 1; c: 3; b: 2 }')
  is(rule.nodes[1].raws.before, ' ')
})

test('insertBefore() works with nodes too', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  rule.insertBefore(rule.nodes[1], { prop: 'c', value: '3' })
  is(rule.toString(), 'a { a: 1; c: 3; b: 2 }')
})

test('insertBefore() receive hash instead of declaration', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  rule.insertBefore(1, { prop: 'c', value: '3' })
  is(rule.toString(), 'a { a: 1; c: 3; b: 2 }')
})

test('insertBefore() receives array', () => {
  let a = parse('a{ color: red; z-index: 1 }')
  let b = parse('b{ width: 1; height: 2 }')
  let aRule = a.first as Rule
  let bRule = b.first as Rule

  aRule.insertBefore(1, bRule.nodes)
  is(a.toString(), 'a{ color: red; width: 1; height: 2; z-index: 1 }')
})

test('insertBefore() receives pre-existing child node - a', () => {
  let a = parse('a{ align-items: start; color: red; z-index: 1 }')
  let declA = (a.first as Rule).nodes[0]
  let declC = (a.first as Rule).nodes[2]
  declC.before(declA)

  is(a.toString(), 'a{ color: red; align-items: start; z-index: 1 }')
})

test('insertBefore() receives pre-existing child node - b', () => {
  let a = parse('a{ align-items: start; color: red; z-index: 1 }')
  let declA = (a.first as Rule).nodes[0]
  let declC = (a.first as Rule).nodes[2]
  declA.before(declC)

  is(a.toString(), 'a{ z-index: 1; align-items: start; color: red }')
})

test('insertBefore() has defined way of adding newlines', () => {
  let root = parse('a {}')
  root.insertBefore(root.first as Rule, 'b {}')
  root.insertBefore(root.first as Rule, 'c {}')
  is(root.toString(), 'c {}\nb {}\na {}')

  root = parse('other {}a {}')
  root.insertBefore(root.first as Rule, 'b {}')
  root.insertBefore(root.first as Rule, 'c {}')
  is(root.toString(), 'c {}b {}other {}a {}')

  root = parse('other {}\na {}')
  root.insertBefore(root.nodes[1], 'b {}')
  root.insertBefore(root.nodes[1], 'c {}')
  is(root.toString(), 'other {}\nc {}\nb {}\na {}')

  root = parse('other {}a {}')
  root.insertBefore(root.nodes[1], 'b {}')
  root.insertBefore(root.nodes[1], 'c {}')
  is(root.toString(), 'other {}c {}b {}a {}')
})

test('insertAfter() inserts child', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  rule.insertAfter(0, { prop: 'c', value: '3' })
  is(rule.toString(), 'a { a: 1; c: 3; b: 2 }')
  is(rule.nodes[1].raws.before, ' ')
})

test('insertAfter() works with nodes too', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  let aDecl = rule.first as Declaration
  rule.insertAfter(aDecl, { prop: 'c', value: '3' })
  is(rule.toString(), 'a { a: 1; c: 3; b: 2 }')
})

test('insertAfter() receive hash instead of declaration', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  rule.insertAfter(0, { prop: 'c', value: '3' })
  is(rule.toString(), 'a { a: 1; c: 3; b: 2 }')
})

test('insertAfter() receives array', () => {
  let a = parse('a{ color: red; z-index: 1 }')
  let b = parse('b{ width: 1; height: 2 }')
  let aRule = a.first as Rule
  let bRule = b.first as Rule

  aRule.insertAfter(0, bRule.nodes)
  is(a.toString(), 'a{ color: red; width: 1; height: 2; z-index: 1 }')
})

test('insertAfter() receives pre-existing child node - a', () => {
  let a = parse('a{ align-items: start; color: red; z-index: 1 }')
  let declA = (a.first as Rule).nodes[0]
  let declC = (a.first as Rule).nodes[2]
  declC.after(declA)

  is(a.toString(), 'a{ color: red; z-index: 1; align-items: start }')
})

test('insertAfter() receives pre-existing child node - b', () => {
  let a = parse('a{ align-items: start; color: red; z-index: 1 }')
  let declA = (a.first as Rule).nodes[0]
  let declC = (a.first as Rule).nodes[2]
  declA.after(declC)

  is(a.toString(), 'a{ align-items: start; z-index: 1; color: red }')
})

test('removeChild() removes by index', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  rule.removeChild(1)
  is(rule.toString(), 'a { a: 1 }')
})

test('removeChild() removes by node', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  let bDecl = rule.last as Declaration
  rule.removeChild(bDecl)
  is(rule.toString(), 'a { a: 1 }')
})

test('removeChild() cleans parent in removed node', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  let aDecl = rule.first as Declaration
  rule.removeChild(aDecl)
  type(aDecl.parent, 'undefined')
})

test('removeAll() removes all children', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  let decl = rule.first as Declaration
  rule.removeAll()

  type(decl.parent, 'undefined')
  is(rule.toString(), 'a { }')
})

test('replaceValues() replaces strings', () => {
  let css = parse('a{one:1}b{two:1 2}')
  let result = css.replaceValues('1', 'A')

  equal(result, css)
  is(css.toString(), 'a{one:A}b{two:A 2}')
})

test('replaceValues() replaces regpexp', () => {
  let css = parse('a{one:1}b{two:1 2}')
  css.replaceValues(/\d/g, i => i + 'A')
  is(css.toString(), 'a{one:1A}b{two:1A 2A}')
})

test('replaceValues() filters properties', () => {
  let css = parse('a{one:1}b{two:1 2}')
  css.replaceValues('1', { props: ['one'] }, 'A')
  is(css.toString(), 'a{one:A}b{two:1 2}')
})

test('replaceValues() uses fast check', () => {
  let css = parse('a{one:1}b{two:1 2}')
  css.replaceValues('1', { fast: '2' }, 'A')
  is(css.toString(), 'a{one:1}b{two:A 2}')
})

test('any() return true if all children return true', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  is(
    rule.every(i => i.type === 'decl' && /a|b/.test(i.prop)),
    true
  )
  is(
    rule.every(i => i.type === 'decl' && /b/.test(i.prop)),
    false
  )
})

test('some() return true if all children return true', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  is(
    rule.some(i => i.type === 'decl' && i.prop === 'b'),
    true
  )
  is(
    rule.some(i => i.type === 'decl' && i.prop === 'c'),
    false
  )
})

test('index() returns child index', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  is(rule.index(rule.nodes[1]), 1)
})

test('index() returns argument if it is number', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  is(rule.index(2), 2)
})

test('first() works for children-less nodes', () => {
  let atRule = parse('@charset "UTF-*"').first as AtRule
  type(atRule.first, 'undefined')
})

test('last() works for children-less nodes', () => {
  let atRule = parse('@charset "UTF-*"').first as AtRule
  type(atRule.last, 'undefined')
})

test('returns first child', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  let aDecl = rule.first as Declaration
  is(aDecl.prop, 'a')
})

test('returns last child', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  let bDecl = rule.last as Declaration
  is(bDecl.prop, 'b')
})

test('normalize() does not normalize new children with exists before', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  rule.append({ prop: 'c', raws: { before: '\n ' }, value: '3' })
  is(rule.toString(), 'a { a: 1; b: 2;\n c: 3 }')
})

test('forces Declaration#value to be string', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  // @ts-expect-error Testing invalid API
  rule.append({ prop: 'c', value: 3 })
  let aDecl = rule.first as Declaration
  let cDecl = rule.last as Declaration
  type(aDecl.value, 'string')
  type(cDecl.value, 'string')
})

test('updates parent in overrides.nodes in constructor', () => {
  let root = new Root({ nodes: [{ selector: 'a' }] })
  let a = root.first as Rule
  equal(a.parent, root)

  root.append({
    nodes: [{ prop: 'color', value: 'black' }],
    selector: 'b'
  })
  let b = root.last as Rule
  let color = b.first as Declaration
  equal(color.parent, root.last)
})

test('allows to clone nodes', () => {
  let root1 = parse('a { color: black; z-index: 1 } b {}')
  let root2 = new Root({ nodes: root1.nodes })
  is(root1.toString(), 'a { color: black; z-index: 1 } b {}')
  is(root2.toString(), 'a { color: black; z-index: 1 } b {}')
})

test('adopts root-less nodes in constructor instead of cloning them', () => {
  let decl = new Declaration({ prop: 'foo', value: 'bar' })
  let atRule = new AtRule({ name: 'foo', nodes: [decl] })

  // The passed-in instance itself is adopted (its parent is updated), so the
  // caller's reference stays usable rather than being silently cloned (#1987).
  equal(decl.parent, atRule)
  is(atRule.first, decl)

  decl.before(new Declaration({ prop: 'baz', value: 'qux' }))
  equal(atRule.nodes!.length, 2)
  is(atRule.last, decl)
})

test('container.nodes can be sorted', () => {
  let root = parse('@b; @c; @a;')
  let b = root.nodes[0]

  root.nodes.sort((x, y) => {
    return (x as AtRule).name.localeCompare((y as AtRule).name)
  })

  // Sorted nodes are reflected in "toString".
  is(root.toString(), ' @a;@b; @c;')

  // Sorted nodes are reflected in "walk".
  let result: string[] = []
  root.walkAtRules(atRule => {
    result.push(atRule.name.trim())
  })

  is(result.join(' '), 'a b c')

  // Sorted nodes have the corect "index".
  is(root.index(b), 1)

  // Inserting after a sorted node results in the correct order.
  b.after('@d;')
  is(root.toString(), ' @a;@b;@d; @c;')
})

test('ignores undefined on adding', () => {
  let rule = parse('a { a: 1; b: 2 }').first as Rule
  rule.append({ prop: 'c', value: '3' }, undefined)
  rule.prepend(undefined)
  rule.insertAfter(0, undefined)
  rule.insertBefore(0, undefined)
  rule.after(undefined)
  rule.before(undefined)
  is(rule.parent!.toString(), 'a { a: 1; b: 2; c: 3 }')
})

test.run()
