const { test } = require('node:test')
const assert = require('node:assert/strict')

const {
  parseXml,
  parseObjects2,
  parsePoints,
  parseRegions,
  parseLocations,
  parseChampions,
  buildPlacementIndex,
  resolveRegion,
  facetKey,
  buildFacetIndex,
  resolveFacetName,
  slugify,
  decodeEntities,
} = require('../src/utils/spawnAtlasParse')

// These parsers are pure and fs-free precisely so this suite can run in CI,
// where there is no ServUO tree. Every fixture below is a literal excerpt of a
// real shard file, trimmed — not invented shapes.

// ── parseObjects2 ──────────────────────────────────────────────────────────

test('parseObjects2: single type', () => {
  const types = parseObjects2('Jacob:MX=1:SB=0:RT=0:TO=0:KL=0:RK=0:CA=1:DN=-1:DX=-1:SP=1:PR=-1')
  assert.deepEqual(types, [{ type: 'Jacob', max: 1 }])
})

test('parseObjects2: splits six types on :OBJ= and keeps each MX', () => {
  // Verbatim from trammel.xml — the case that a naive split(':') destroys.
  const raw =
    'Gazer:MX=1:SB=0:RT=0:TO=0:KL=0:RK=0:CA=1:DN=-1:DX=-1:SP=1:PR=-1' +
    ':OBJ=Giantspider:MX=1:SB=0:RT=0:TO=0:KL=0:RK=0:CA=1:DN=-1:DX=-1:SP=1:PR=-1' +
    ':OBJ=Harpy:MX=1:SB=0:RT=0:TO=0:KL=0:RK=0:CA=1:DN=-1:DX=-1:SP=1:PR=-1' +
    ':OBJ=Headlessone:MX=1:SB=0:RT=0:TO=0:KL=0:RK=0:CA=1:DN=-1:DX=-1:SP=1:PR=-1' +
    ':OBJ=Lizardman:MX=3:SB=0:RT=0:TO=0:KL=0:RK=0:CA=1:DN=-1:DX=-1:SP=1:PR=-1' +
    ':OBJ=Mongbat:MX=1:SB=0:RT=0:TO=0:KL=0:RK=0:CA=1:DN=-1:DX=-1:SP=1:PR=-1'
  const types = parseObjects2(raw)
  assert.equal(types.length, 6)
  assert.deepEqual(
    types.map((t) => t.type),
    ['Gazer', 'Giantspider', 'Harpy', 'Headlessone', 'Lizardman', 'Mongbat'],
  )
  // MX is per type, not per spawner: the lizardman entry carries 3.
  assert.equal(types.find((t) => t.type === 'Lizardman').max, 3)
  assert.equal(types.find((t) => t.type === 'Gazer').max, 1)
})

test('parseObjects2: empty and whitespace values yield no types', () => {
  assert.deepEqual(parseObjects2(''), [])
  assert.deepEqual(parseObjects2('   '), [])
  assert.deepEqual(parseObjects2(null), [])
  assert.deepEqual(parseObjects2(undefined), [])
})

test('parseObjects2: strips XmlSpawner property directives after "/"', () => {
  // Left in place these become creatures that do not exist.
  assert.deepEqual(parseObjects2('Agralem/Name/Agralem:MX=1'), [{ type: 'Agralem', max: 1 }])
  assert.deepEqual(parseObjects2('alchemist/z/-50:MX=1'), [{ type: 'alchemist', max: 1 }])
  assert.deepEqual(parseObjects2('GargishRefugee/hue/34532'), [
    { type: 'GargishRefugee', max: 1 },
  ])
})

test('parseObjects2: strips argument lists after ","', () => {
  assert.deepEqual(parseObjects2('Fairy,{RND,4,8}:MX=1'), [{ type: 'Fairy', max: 1 }])
  assert.deepEqual(parseObjects2('GargishRouser,1'), [{ type: 'GargishRouser', max: 1 }])
  assert.deepEqual(parseObjects2('greatape,true'), [{ type: 'greatape', max: 1 }])
})

test('parseObjects2: a directive-laden token slugs the same as the bare one', () => {
  // The bug this closes: `Fairy` and `Fairy,{RND,4,8}` slugged apart and showed
  // as two different creatures on the same page.
  const bare = parseObjects2('Fairy:MX=1')[0]
  const decorated = parseObjects2('Fairy,{RND,4,8}:MX=1')[0]
  assert.equal(slugify(decorated.type), slugify(bare.type))
})

test('parseObjects2: strips a long EQUIP directive chain containing "<" and ">"', () => {
  const raw =
    'xmlquestnpc/UNEQUIP,Innertorso/UNEQUIP,MiddleTorso/EQUIP/<robe/loottype/blessed' +
    '/itemid/8259>/blessed/true/name/lord blackthorne/z/:MX=1'
  assert.deepEqual(parseObjects2(raw), [{ type: 'xmlquestnpc', max: 1 }])
})

test('parseObjects2: a token that is only a directive yields nothing', () => {
  assert.deepEqual(parseObjects2('/Name/Foo:MX=1'), [])
  assert.deepEqual(parseObjects2(',1:MX=1'), [])
})

test('parseObjects2: a type with no MX token defaults to 1', () => {
  assert.deepEqual(parseObjects2('Orc'), [{ type: 'Orc', max: 1 }])
  assert.deepEqual(parseObjects2('Orc:SB=0:RT=0'), [{ type: 'Orc', max: 1 }])
})

// ── parsePoints ────────────────────────────────────────────────────────────

const POINTS_XML = `<Spawns>
  <Points>
    <Name>CovetousSpawner26</Name>
    <UniqueId>001a34e5-0efa-46de-9c93-b6a163d96370</UniqueId>
    <Map>Trammel</Map>
    <X>5412</X>
    <Y>1970</Y>
    <Width>10</Width>
    <Height>10</Height>
    <Range>5</Range>
    <MaxCount>3</MaxCount>
    <MinDelay>5</MinDelay>
    <MaxDelay>10</MaxDelay>
    <ProximityTriggerSound>500</ProximityTriggerSound>
    <TODStart>0</TODStart>
    <TODEnd>0</TODEnd>
    <TODMode>0</TODMode>
    <IsRunning>True</IsRunning>
    <Objects2>Lizardman:MX=3:SB=0</Objects2>
  </Points>
  <Points>
    <Name>Disabled</Name>
    <Map>Felucca</Map>
    <X>100</X>
    <Y>200</Y>
    <MaxCount>1</MaxCount>
    <IsRunning>False</IsRunning>
    <Objects2>Orc:MX=1</Objects2>
  </Points>
</Spawns>`

test('parsePoints: reads the kept fields and drops the rest', () => {
  const points = parsePoints(POINTS_XML)
  assert.equal(points.length, 2)
  const covetous = points[0]
  assert.equal(covetous.name, 'CovetousSpawner26')
  assert.equal(covetous.facet, 'Trammel')
  assert.equal(covetous.x, 5412)
  assert.equal(covetous.y, 1970)
  assert.equal(covetous.width, 10)
  assert.equal(covetous.range, 5)
  assert.equal(covetous.maxCount, 3)
  // Delays are normalised to seconds; this record carries no DelayInSec, which
  // means minutes.
  assert.equal(covetous.minDelay, 300)
  assert.equal(covetous.maxDelay, 600)
  assert.deepEqual(covetous.types, [{ type: 'Lizardman', max: 3 }])
  // Dropped fields must not survive into the artifact — this is what keeps it
  // under 1 MB.
  assert.equal(covetous.uniqueId, undefined)
  assert.equal(covetous.proximityTriggerSound, undefined)
})

test('parsePoints: IsRunning is parsed so the build can drop dead spawners', () => {
  const points = parsePoints(POINTS_XML)
  assert.equal(points[0].running, true)
  assert.equal(points[1].running, false)
})

test('parsePoints: facet comes from <Map>, never the file name', () => {
  // Eodon.xml holds TerMur points; a file-name assumption would mislabel every
  // one of them.
  const points = parsePoints(
    '<Spawns><Points><Name>a</Name><Map>TerMur</Map><X>1</X><Y>2</Y></Points></Spawns>',
  )
  assert.equal(points[0].facet, 'TerMur')
})

test('parsePoints: a record with no <Map> is skipped rather than misfiled', () => {
  const points = parsePoints('<Spawns><Points><Name>a</Name><X>1</X><Y>2</Y></Points></Spawns>')
  assert.deepEqual(points, [])
})

test('parsePoints: empty document yields no points', () => {
  assert.deepEqual(parsePoints('<Spawns></Spawns>'), [])
  assert.deepEqual(parsePoints(''), [])
})

// ── parseRegions ───────────────────────────────────────────────────────────

const REGIONS_XML = `<?xml version="1.0" encoding="utf-8"?>
<ServerRegions>
  <Facet name="Felucca">
    <region type="GuardedRegion" priority="50" name="Moongates">
      <!-- britain -->
      <rect x="1330" y="1991" width="13" height="13" />
      <rect x="761" y="741" width="19" height="21" />
    </region>
    <region type="MondainRegion" priority="50" name="Prism of Light">
      <rect x="6400" y="0" width="221" height="255" />
      <go x="6474" y="188" z="0" />
      <music name="Dungeon9" />
      <region type="CrystalField" name="Crystal Field">
        <rect x="6506" y="83" width="7" height="7" />
        <zrange min="-4" />
      </region>
      <region type="IcyRiver">
        <rect x="6576" y="73" width="10" height="31" />
      </region>
    </region>
    <region type="TownRegion" priority="10" name="Music Only">
      <music name="Britain" />
    </region>
  </Facet>
</ServerRegions>`

test('parseRegions: flattens nested regions and collects rects', () => {
  const regions = parseRegions(REGIONS_XML)
  const byName = new Map(regions.map((r) => [r.name, r]))
  assert.ok(byName.has('Moongates'))
  assert.ok(byName.has('Prism of Light'))
  assert.equal(byName.get('Moongates').rects.length, 2)
  assert.deepEqual(byName.get('Moongates').rects[0], {
    x: 1330,
    y: 1991,
    width: 13,
    height: 13,
  })
  assert.equal(byName.get('Moongates').facet, 'Felucca')
  assert.equal(byName.get('Moongates').type, 'GuardedRegion')
})

test('parseRegions: a nested child records its parent', () => {
  const regions = parseRegions(REGIONS_XML)
  const crystal = regions.find((r) => r.name === 'Crystal Field')
  assert.ok(crystal, 'the nested named region should be indexed')
  assert.equal(crystal.parent, 'Prism of Light')
  assert.equal(crystal.facet, 'Felucca')
})

test('parseRegions: a child with no priority inherits its parent', () => {
  // Defaulting to 0 instead would sort this specific room below every
  // top-level region that contains it.
  const crystal = parseRegions(REGIONS_XML).find((r) => r.name === 'Crystal Field')
  assert.equal(crystal.priority, 50)
})

test('parseRegions: unnamed regions are skipped but still walked', () => {
  const regions = parseRegions(REGIONS_XML)
  // IcyRiver has a type but no name — it cannot label anything.
  assert.equal(regions.some((r) => r.type === 'IcyRiver'), false)
})

test('parseRegions: a named region with no rects is not indexed', () => {
  // It can never contain a point, so indexing it only costs scan time.
  assert.equal(parseRegions(REGIONS_XML).some((r) => r.name === 'Music Only'), false)
})

// ── parseLocations ─────────────────────────────────────────────────────────

const LOCATIONS_XML = `<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<places>
  <parent name="Trammel">
    <parent name="Dungeons">
      <parent name="Covetous">
        <child name="Entrance" x="2499" y="919" z="0" />
        <child name="Level 1" x="5456" y="1863" z="0" />
      </parent>
      <parent name="Despise">
        <child name="Level 3" x="5407" y="859" z="45" />
      </parent>
    </parent>
  </parent>
</places>`

test('parseLocations: flattens to points carrying their group', () => {
  const landmarks = parseLocations(LOCATIONS_XML)
  assert.equal(landmarks.length, 3)
  const level1 = landmarks.find((l) => l.name === 'Level 1')
  assert.equal(level1.x, 5456)
  assert.equal(level1.y, 1863)
  assert.equal(level1.z, 0)
  assert.equal(level1.facet, 'Trammel')
  // "Covetous" is the useful label, not "Level 1".
  assert.equal(level1.group, 'Covetous')
  // The facet-level parent is dropped from the path.
  assert.deepEqual(level1.path, ['Dungeons', 'Covetous'])
})

// ── Facet canonicalisation ─────────────────────────────────────────────────

// Facets are NOT a fixed list — a shard may add, replace or rename them when its
// maps are updated, so nothing may hardcode the stock six. Reconciliation is by
// matching against whatever the shard's own files declare.

test('facetKey: collapses spelling differences to one key', () => {
  assert.equal(facetKey('Ter Mur'), facetKey('TerMur'))
  assert.equal(facetKey('ter-mur'), facetKey('TerMur'))
  assert.equal(facetKey('Felucca'), 'felucca')
  assert.equal(facetKey(''), '')
  assert.equal(facetKey(null), '')
})

test('facetKey: distinct facets keep distinct keys', () => {
  assert.notEqual(facetKey('Felucca'), facetKey('Trammel'))
})

test('resolveFacetName: matches a loose spelling to the discovered canonical', () => {
  // The canonical set comes from the shard's own spawn/region data, not a table.
  const index = buildFacetIndex(['TerMur', 'Tokuno', 'Felucca'])
  assert.equal(resolveFacetName('Ter Mur', index), 'TerMur')
  assert.equal(resolveFacetName('Tokuno Islands', index), 'Tokuno')
  assert.equal(resolveFacetName('felucca', index), 'Felucca')
})

test('resolveFacetName: works for facets that do not exist in stock UO', () => {
  // The whole point: a shard running its own maps gets the same treatment as
  // the stock ones, with no entry anywhere naming them.
  const index = buildFacetIndex(['Sosaria', 'The Underdark'])
  assert.equal(resolveFacetName('sosaria', index), 'Sosaria')
  assert.equal(resolveFacetName('The  Underdark', index), 'The Underdark')
  assert.equal(resolveFacetName('the-underdark', index), 'The Underdark')
  // Same shape as the real `Tokuno Islands` → `Tokuno` case.
  assert.equal(resolveFacetName('Sosaria Isles', index), 'Sosaria')
})

test('resolveFacetName: a merely similar name is NOT forced to match', () => {
  // "Underdark Isles" is not a prefix of "The Underdark" in either direction.
  // Keeping its own name is right — a wrong match would silently file a real
  // custom facet's landmarks under the wrong facet.
  const index = buildFacetIndex(['The Underdark'])
  assert.equal(resolveFacetName('Underdark Isles', index), 'Underdark Isles')
})

test('resolveFacetName: prefers the longer match when several could prefix', () => {
  const index = buildFacetIndex(['Tokuno', 'TokunoDeep'])
  assert.equal(resolveFacetName('TokunoDeep Reaches', index), 'TokunoDeep')
})

test('resolveFacetName: an unmatched facet keeps its own name', () => {
  // Inventing a match would be worse than leaving a real custom facet alone.
  const index = buildFacetIndex(['Felucca'])
  assert.equal(resolveFacetName('Ilshenar', index), 'Ilshenar')
  assert.equal(resolveFacetName('', index), '')
  assert.equal(resolveFacetName(null, index), '')
})

test('buildFacetIndex: first spelling wins and is stable', () => {
  const index = buildFacetIndex(['TerMur', 'Ter Mur', 'ter-mur'])
  assert.equal(index.size, 1)
  assert.equal(resolveFacetName('Ter Mur', index), 'TerMur')
})

test('parsePoints and parseRegions report facet names verbatim', () => {
  // <Map> and <Facet name> are the authority; they are never rewritten.
  const points = parsePoints(
    '<Spawns><Points><Name>a</Name><Map>Sosaria</Map><X>1</X><Y>2</Y></Points></Spawns>',
  )
  assert.equal(points[0].facet, 'Sosaria')
  const regions = parseRegions(
    '<ServerRegions><Facet name="Sosaria"><region name="Town" priority="1">' +
      '<rect x="0" y="0" width="10" height="10"/></region></Facet></ServerRegions>',
  )
  assert.equal(regions[0].facet, 'Sosaria')
})

test('placement index buckets two spellings of one facet together', () => {
  // This is the bug the key exists to prevent: unreconciled, the landmark bucket
  // is keyed apart from the points looking it up, the fallback never fires, and
  // every unregioned spawn on that facet silently reads "Wilderness".
  const index = buildPlacementIndex(
    [],
    [{ facet: 'Ter Mur', name: 'Bank', group: 'Holy City', path: [], x: 1000, y: 1000, z: 0 }],
  )
  assert.equal(resolveRegion(1000, 1000, 'TerMur', index).landmark, 'Holy City')
})

// ── parseChampions ─────────────────────────────────────────────────────────

const CHAMPIONS_XML = `<?xml version="1.0" encoding="UTF-8"?>
<championSystem>
  <!-- comment describing the schema -->
  <spawn name="Deceit" group="FelDungeons" type="UnholyTerror">
    <location x="5178" y="708" z="20" map="Felucca" radius="60" />
  </spawn>
  <spawn name="Wandering" group="FelDungeons">
    <location x="100" y="200" z="0" map="Felucca" radius="40" />
  </spawn>
</championSystem>`

test('parseChampions: reads altar name, type and location', () => {
  const champs = parseChampions(CHAMPIONS_XML)
  assert.equal(champs.length, 2)
  assert.deepEqual(champs[0], {
    name: 'Deceit',
    group: 'FelDungeons',
    type: 'UnholyTerror',
    randomType: false,
    facet: 'Felucca',
    x: 5178,
    y: 708,
    z: 20,
    radius: 60,
  })
})

test('parseChampions: a spawn with no type is flagged random, not blank', () => {
  const champs = parseChampions(CHAMPIONS_XML)
  assert.equal(champs[1].randomType, true)
  assert.equal(champs[1].type, '')
})

// ── resolveRegion ──────────────────────────────────────────────────────────

function fixtureIndex() {
  const regions = [
    {
      facet: 'Felucca',
      name: 'Britain',
      type: 'TownRegion',
      priority: 10,
      parent: null,
      rects: [{ x: 1000, y: 1000, width: 500, height: 500 }],
    },
    {
      facet: 'Felucca',
      name: 'Britain Bank',
      type: 'TownRegion',
      priority: 50,
      parent: 'Britain',
      rects: [{ x: 1400, y: 1400, width: 20, height: 20 }],
    },
    {
      facet: 'Felucca',
      name: 'Wide Low Priority',
      type: 'TownRegion',
      priority: 10,
      parent: null,
      rects: [{ x: 1000, y: 1000, width: 2000, height: 2000 }],
    },
  ]
  const landmarks = [
    { facet: 'Felucca', name: 'Level 1', group: 'Covetous', path: [], x: 5000, y: 5000, z: 0 },
    { facet: 'Felucca', name: 'Far Away', group: 'Vesper', path: [], x: 9000, y: 9000, z: 0 },
  ]
  return buildPlacementIndex(regions, landmarks)
}

test('resolveRegion: a contained point takes the region name', () => {
  const result = resolveRegion(1100, 1100, 'Felucca', fixtureIndex())
  assert.equal(result.region, 'Britain')
  assert.equal(result.label, 'Britain')
  assert.equal(result.landmark, null)
})

test('resolveRegion: higher priority wins over a containing region', () => {
  const result = resolveRegion(1410, 1410, 'Felucca', fixtureIndex())
  assert.equal(result.region, 'Britain Bank')
})

test('resolveRegion: equal priority breaks toward the smaller rect', () => {
  // Both "Britain" (500x500) and "Wide Low Priority" (2000x2000) contain this
  // point at priority 10; the specific one must win.
  const result = resolveRegion(1200, 1200, 'Felucca', fixtureIndex())
  assert.equal(result.region, 'Britain')
})

test('resolveRegion: rects are half-open — the far edge is outside', () => {
  const index = fixtureIndex()
  // Britain spans x 1000..1499. 1499 is in, 1500 belongs to the next region.
  assert.equal(resolveRegion(1499, 1499, 'Felucca', index).region, 'Britain')
  assert.equal(resolveRegion(1500, 1500, 'Felucca', index).region, 'Wide Low Priority')
})

test('resolveRegion: falls back to the nearest landmark group', () => {
  const result = resolveRegion(5050, 5050, 'Felucca', fixtureIndex())
  assert.equal(result.region, null)
  assert.equal(result.landmark, 'Covetous')
  assert.equal(result.label, 'Covetous')
})

test('resolveRegion: a landmark beyond the radius yields Wilderness', () => {
  // Without the radius cap the nearest landmark is always *some* landmark, and
  // open countryside would get labelled with a dungeon across the map.
  const result = resolveRegion(7000, 7000, 'Felucca', fixtureIndex())
  assert.equal(result.landmark, null)
  assert.equal(result.label, 'Wilderness')
})

test('resolveRegion: the radius is configurable', () => {
  const wide = resolveRegion(7000, 7000, 'Felucca', fixtureIndex(), { landmarkRadius: 5000 })
  assert.equal(wide.label, 'Covetous')
})

test('resolveRegion: an unknown facet degrades to Wilderness, not a throw', () => {
  const result = resolveRegion(1100, 1100, 'Malas', fixtureIndex())
  assert.equal(result.label, 'Wilderness')
  assert.equal(result.region, null)
})

test('resolveRegion: does not leak across facets', () => {
  const index = buildPlacementIndex(
    [
      {
        facet: 'Trammel',
        name: 'Britain',
        type: 'TownRegion',
        priority: 10,
        parent: null,
        rects: [{ x: 1000, y: 1000, width: 500, height: 500 }],
      },
    ],
    [],
  )
  assert.equal(resolveRegion(1100, 1100, 'Trammel', index).region, 'Britain')
  assert.equal(resolveRegion(1100, 1100, 'Felucca', index).region, null)
})

// ── Tokenizer edge cases ───────────────────────────────────────────────────

test('parseXml: skips comments, declarations and DOCTYPE', () => {
  const root = parseXml(
    '<?xml version="1.0"?><!DOCTYPE r><r><!-- <fake a="b"/> --><a x="1"/></r>',
  )
  assert.equal(root.name, 'r')
  assert.equal(root.children.length, 1)
  assert.equal(root.children[0].name, 'a')
  assert.equal(root.children[0].attrs.x, '1')
})

test('parseXml: a ">" inside an attribute value does not end the tag', () => {
  const root = parseXml('<r><a name="1 > 0" b="2"/></r>')
  assert.equal(root.children[0].attrs.name, '1 > 0')
  assert.equal(root.children[0].attrs.b, '2')
})

test('parseXml: single-quoted attributes are read', () => {
  const root = parseXml("<r><a name='Mondain' /></r>")
  assert.equal(root.children[0].attrs.name, 'Mondain')
})

test('parseXml: a stray closing tag is ignored, not fatal', () => {
  // Hand-maintained shard config: one malformed element should degrade to a
  // missing element, not abort an otherwise good build.
  const root = parseXml('<r><a/></b><c/></r>')
  assert.equal(root.name, 'r')
  assert.deepEqual(root.children.map((n) => n.name), ['a', 'c'])
})

test('parseXml: empty or element-free input yields null', () => {
  assert.equal(parseXml(''), null)
  assert.equal(parseXml('<!-- only a comment -->'), null)
})

test('decodeEntities: named, numeric and hex refs', () => {
  assert.equal(decodeEntities("Mondain&apos;s Legacy"), "Mondain's Legacy")
  assert.equal(decodeEntities('a &amp; b'), 'a & b')
  assert.equal(decodeEntities('&lt;tag&gt;'), '<tag>')
  assert.equal(decodeEntities('&#65;&#x42;'), 'AB')
  // An unknown entity is left alone rather than silently eaten.
  assert.equal(decodeEntities('&nosuch;'), '&nosuch;')
})

test('parseXml: decodes entities in attribute values', () => {
  const root = parseXml('<r><region name="Mondain&apos;s Legacy" /></r>')
  assert.equal(root.children[0].attrs.name, "Mondain's Legacy")
})

// ── slugify ────────────────────────────────────────────────────────────────

test('slugify: produces URL-safe keys', () => {
  assert.equal(slugify('Lizardman'), 'lizardman')
  assert.equal(slugify('Giant Spider'), 'giant-spider')
  assert.equal(slugify("Mondain's Legacy"), 'mondain-s-legacy')
  assert.equal(slugify('  Orc  '), 'orc')
})

// ── Respawn delays: the unit is per record ──────────────────────────────────
// XmlSpawner writes minutes by default and switches to seconds only when a
// delay does not divide into whole minutes, flagged by DelayInSec. `5` therefore
// means five MINUTES on one spawner and five SECONDS on the next, and a reader
// assuming either unit is wrong about the other — silently, since both are
// plausible respawn times.
const DELAY_XML = `<Spawns>
  <Points>
    <Name>Minutes</Name>
    <Map>Sosaria</Map>
    <X>1</X><Y>1</Y>
    <MinDelay>5</MinDelay>
    <MaxDelay>10</MaxDelay>
    <IsRunning>True</IsRunning>
    <Objects2>Orc:MX=1</Objects2>
  </Points>
  <Points>
    <Name>Seconds</Name>
    <Map>Sosaria</Map>
    <X>2</X><Y>2</Y>
    <DelayInSec>True</DelayInSec>
    <MinDelay>5</MinDelay>
    <MaxDelay>10</MaxDelay>
    <IsRunning>True</IsRunning>
    <Objects2>Orc:MX=1</Objects2>
  </Points>
</Spawns>`

test('parsePoints: DelayInSec decides the unit, and both come out in seconds', () => {
  const [minutes, seconds] = parsePoints(DELAY_XML)
  assert.equal(minutes.minDelay, 300)
  assert.equal(minutes.maxDelay, 600)
  assert.equal(seconds.minDelay, 5)
  assert.equal(seconds.maxDelay, 10)
})
