import { describe, it, expect } from 'vitest'
import {
  blankEntry,
  splitList,
  buildEntryPayload,
  availableEntryTypes,
  availableDetectionMethods,
  targetingGates,
  targetingHint,
  scopeSummary,
  describeEntry,
  detectionMethodHint,
  ENTRY_TYPES,
  DETECTION_METHODS,
} from './entryForm.js'

// These specs pin the deterministic logic that turns the manifest editor form
// into an API body, and the various display helpers. This is the code most
// likely to silently regress (numeric coercion, dropped-vs-kept fields, the
// appid-must-never-be-a-manifest-scalar rule).

describe('splitList', () => {
  it('trims, drops empties, and returns an array', () => {
    expect(splitList('a, b ,,  c ')).toEqual(['a', 'b', 'c'])
  })
  it('returns an empty array for null/empty', () => {
    expect(splitList('')).toEqual([])
    expect(splitList(null)).toEqual([])
    expect(splitList(undefined)).toEqual([])
  })
})

describe('buildEntryPayload - appid handling', () => {
  it('always emits appid as a top-level key, defaulting to null', () => {
    const out = buildEntryPayload({ Name: 'x', Type: 'MSI' })
    expect(out.appid).toBeNull()
    expect('appid' in out).toBe(true)
  })
  it('passes a linked appid through unchanged', () => {
    const out = buildEntryPayload({ Name: 'x', Type: 'MSI', appid: 41 })
    expect(out.appid).toBe(41)
  })
  it('never lets appid become a manifest scalar (it is metadata, not a manifest key)', () => {
    // appid lives at the top level as metadata; it must not appear nested in
    // any manifest sub-structure. Guard against a future refactor smuggling it
    // into e.g. InUseCheck or a detection block.
    const out = buildEntryPayload({
      Name: 'x', Type: 'Registry', appid: 7, RegType: 'DWord', RegValue: '1',
      DetectionMethod: 'Registry', inuseBehavior: 'ForceClose',
      inuseProcesses: [{ name: 'p', exepath: '', timeout: null }],
    })
    // Only the top-level appid key carries it.
    const serialized = JSON.stringify({ ...out, appid: undefined })
    expect(serialized.includes('appid')).toBe(false)
    expect(serialized.toLowerCase().includes('"appid"')).toBe(false)
  })
})

describe('buildEntryPayload - scalar filtering', () => {
  it('drops empty-string, null, and undefined scalars', () => {
    const out = buildEntryPayload({
      Name: 'x', Type: 'MSI', Installer: '', InstallArgs: null,
      Script: undefined, LogFile: 'setup.log',
    })
    expect('Installer' in out).toBe(false)
    expect('InstallArgs' in out).toBe(false)
    expect('Script' in out).toBe(false)
    expect(out.LogFile).toBe('setup.log')
  })
  it('keeps the underscore-prefixed scalars (_CmmVersion, _comment)', () => {
    const out = buildEntryPayload({
      Name: 'x', Type: 'MSI', _CmmVersion: '2019', _comment: 'note',
    })
    expect(out._CmmVersion).toBe('2019')
    expect(out._comment).toBe('note')
  })
  it('only emits DetectionMethod when set', () => {
    expect('DetectionMethod' in buildEntryPayload({ Name: 'x', Type: 'MSI', DetectionMethod: '' })).toBe(false)
    expect(buildEntryPayload({ Name: 'x', Type: 'MSI', DetectionMethod: 'FileVersion' }).DetectionMethod).toBe('FileVersion')
  })
  it('drops a zero/falsy WaitTimeoutSec but keeps a real one', () => {
    expect('WaitTimeoutSec' in buildEntryPayload({ Name: 'x', Type: 'MSI', WaitTimeoutSec: 0 })).toBe(false)
    expect(buildEntryPayload({ Name: 'x', Type: 'MSI', WaitTimeoutSec: 300 }).WaitTimeoutSec).toBe(300)
  })
})

describe('buildEntryPayload - RegValue coercion', () => {
  it('coerces DWord to a Number', () => {
    const out = buildEntryPayload({ Name: 'x', Type: 'Registry', RegType: 'DWord', RegValue: '1' })
    expect(out.RegValue).toBe(1)
    expect(typeof out.RegValue).toBe('number')
  })
  it('coerces QWord to a Number', () => {
    const out = buildEntryPayload({ Name: 'x', Type: 'Registry', RegType: 'QWord', RegValue: '42' })
    expect(out.RegValue).toBe(42)
  })
  it('leaves String reg values as strings', () => {
    const out = buildEntryPayload({ Name: 'x', Type: 'Registry', RegType: 'String', RegValue: 'hello' })
    expect(out.RegValue).toBe('hello')
    expect(typeof out.RegValue).toBe('string')
  })
  it('does not emit RegValue for a non-Registry type', () => {
    const out = buildEntryPayload({ Name: 'x', Type: 'MSI', RegType: 'DWord', RegValue: '1' })
    expect('RegValue' in out).toBe(false)
  })
  it('drops an empty RegValue even for Registry', () => {
    const out = buildEntryPayload({ Name: 'x', Type: 'Registry', RegType: 'DWord', RegValue: '' })
    expect('RegValue' in out).toBe(false)
  })
})

describe('buildEntryPayload - comma lists to arrays', () => {
  it('splits PCTypes / TargetHostnames / TargetMachineNumbers', () => {
    const out = buildEntryPayload({
      Name: 'x', Type: 'MSI',
      PCTypes: 'gea-shopfloor-cmm, gea-shopfloor-collections',
      TargetHostnames: 'host-a, host-b',
      TargetMachineNumbers: '0101, 0102',
    })
    expect(out.PCTypes).toEqual(['gea-shopfloor-cmm', 'gea-shopfloor-collections'])
    expect(out.TargetHostnames).toEqual(['host-a', 'host-b'])
    expect(out.TargetMachineNumbers).toEqual(['0101', '0102'])
  })
  it('omits the array keys when the list is empty', () => {
    const out = buildEntryPayload({ Name: 'x', Type: 'MSI', PCTypes: '', TargetHostnames: '  ' })
    expect('PCTypes' in out).toBe(false)
    expect('TargetHostnames' in out).toBe(false)
  })
})

describe('buildEntryPayload - preinstall flags', () => {
  it('emits only the truthy flags', () => {
    const out = buildEntryPayload({
      Name: 'x', Type: 'MSI',
      PreEnrollment: true, KillAfterDetection: false, PCTypesStrict: true,
    })
    expect(out.PreEnrollment).toBe(true)
    expect(out.PCTypesStrict).toBe(true)
    expect('KillAfterDetection' in out).toBe(false)
  })
})

describe('buildEntryPayload - InUseCheck processes', () => {
  it('carries Name, optional ExePath, and a numeric timeout per process', () => {
    const out = buildEntryPayload({
      Name: 'x', Type: 'MSI', inuseBehavior: 'CloseAndReopen',
      inuseProcesses: [
        { name: 'pcdmis', exepath: 'C:\\pcd\\pcdmis.exe', timeout: 30 },
        { name: 'nodmis', exepath: '', timeout: null },
      ],
    })
    expect(out.InUseCheck.Behavior).toBe('CloseAndReopen')
    expect(out.InUseCheck.Processes).toEqual([
      { Name: 'pcdmis', ExePath: 'C:\\pcd\\pcdmis.exe', GracefulCloseTimeoutSec: 30 },
      { Name: 'nodmis' },
    ])
  })
  it('drops processes with no name', () => {
    const out = buildEntryPayload({
      Name: 'x', Type: 'MSI', inuseBehavior: 'ForceClose',
      inuseProcesses: [{ name: '', exepath: 'x', timeout: 5 }, { name: 'keep', timeout: 5 }],
    })
    expect(out.InUseCheck.Processes).toEqual([{ Name: 'keep', GracefulCloseTimeoutSec: 5 }])
  })
  it('coerces a string timeout to a number', () => {
    const out = buildEntryPayload({
      Name: 'x', Type: 'MSI', inuseBehavior: 'Defer',
      inuseProcesses: [{ name: 'p', exepath: '', timeout: '15' }],
    })
    expect(out.InUseCheck.Processes[0].GracefulCloseTimeoutSec).toBe(15)
  })
  it('emits no InUseCheck when there is no behavior', () => {
    const out = buildEntryPayload({ Name: 'x', Type: 'MSI', inuseBehavior: '' })
    expect('InUseCheck' in out).toBe(false)
  })
})

describe('buildEntryPayload - a blank new entry round-trips', () => {
  it('produces a minimal body from blankEntry (name/type + null appid + default RegType)', () => {
    const out = buildEntryPayload({ ...blankEntry(), Name: 'Fresh' })
    // Type MSI, appid null. RegType is default String but non-Registry, so it
    // is still carried as a scalar (it is in the scalar list). Confirm the shape.
    expect(out.Name).toBe('Fresh')
    expect(out.Type).toBe('MSI')
    expect(out.appid).toBeNull()
    expect('InUseCheck' in out).toBe(false)
    expect('PCTypes' in out).toBe(false)
  })
})

describe('availableEntryTypes', () => {
  it('returns the full list for a non-preinstall scope', () => {
    expect(availableEntryTypes(false, 'File')).toEqual(ENTRY_TYPES)
  })
  it('restricts to MSI/EXE for preinstall', () => {
    expect(availableEntryTypes(true, 'MSI')).toEqual(['MSI', 'EXE'])
  })
  it('keeps an out-of-set current value visible in preinstall', () => {
    expect(availableEntryTypes(true, 'Registry')).toEqual(['MSI', 'EXE', 'Registry'])
  })
})

describe('availableDetectionMethods', () => {
  it('returns the full list for a non-preinstall scope', () => {
    expect(availableDetectionMethods(false, 'Hash')).toEqual(DETECTION_METHODS)
  })
  it('restricts to Registry/File for preinstall', () => {
    expect(availableDetectionMethods(true, 'File')).toEqual(['Registry', 'File'])
  })
  it('keeps an out-of-set current value visible in preinstall', () => {
    expect(availableDetectionMethods(true, 'FileVersion')).toEqual(['Registry', 'File', 'FileVersion'])
  })
})

describe('targetingGates', () => {
  it('defaults to pctypes-only when there is no scope', () => {
    expect(targetingGates(null)).toEqual({
      pctypes: true, cmmversion: false, machinenumbers: false, hostnames: false,
    })
  })
  it('shows pctypes for a fleetwide/common/preinstall scope', () => {
    expect(targetingGates({ scopename: 'common', entries: [] }).pctypes).toBe(true)
    expect(targetingGates({ scopename: 'x', phase: 'preinstall', entries: [] }).pctypes).toBe(true)
    expect(targetingGates({ scopename: 'x', iscommon: true, entries: [] }).pctypes).toBe(true)
  })
  it('shows the cmm gate for a cmm-named scope', () => {
    expect(targetingGates({ scopename: 'gea-shopfloor-cmm', entries: [] }).cmmversion).toBe(true)
  })
  it('does not name-match machinenumbers (data-driven only)', () => {
    // 'nocollections' must not trip a loose name match; machinenumbers is purely
    // data-driven off the entries.
    const gates = targetingGates({ scopename: 'nocollections', entries: [] })
    expect(gates.machinenumbers).toBe(false)
  })
  it('surfaces gates from entry data', () => {
    const gates = targetingGates({
      scopename: 'per-type', entries: [
        { PCTypes: ['a'] }, { TargetMachineNumbers: ['0101'] }, { TargetHostnames: ['h'] },
      ],
    })
    expect(gates.pctypes).toBe(true)
    expect(gates.machinenumbers).toBe(true)
    expect(gates.hostnames).toBe(true)
  })
})

describe('targetingHint', () => {
  it('is empty with no scope', () => {
    expect(targetingHint(null)).toBe('')
  })
  it('describes a fleet-wide common manifest', () => {
    expect(targetingHint({ scopename: 'common' })).toMatch(/Fleet-wide/)
  })
  it('describes a preinstall manifest', () => {
    expect(targetingHint({ scopename: 'x', phase: 'preinstall' })).toMatch(/Preinstall/)
  })
  it('names a per-type scope', () => {
    expect(targetingHint({ scopename: 'gea-shopfloor-cmm' })).toMatch(/gea-shopfloor-cmm/)
  })
})

describe('scopeSummary', () => {
  it('is empty with no entries', () => {
    expect(scopeSummary({ entries: [] })).toBe('')
    expect(scopeSummary(null)).toBe('')
  })
  it('lists installer names and the entry count', () => {
    const summary = scopeSummary({
      entries: [
        { Type: 'MSI', Name: 'eDNC' },
        { Type: 'Registry', Name: 'reg' },
      ],
    })
    expect(summary).toBe('Installs eDNC. 2 entries; runs after common.')
  })
  it('truncates past six installers with a +N more', () => {
    const entries = Array.from({ length: 8 }, (_, i) => ({ Type: 'MSI', Name: `app${i}` }))
    const summary = scopeSummary({ entries })
    expect(summary).toMatch(/\+2 more/)
  })
})

describe('describeEntry', () => {
  it('describes an MSI with FileVersion detection using the expected version', () => {
    const text = describeEntry({ Type: 'MSI', Name: 'PC-DMIS', DetectionMethod: 'FileVersion', DetectionValue: '2019 R1' })
    expect(text).toBe('Installs PC-DMIS; reinstalls unless version is 2019 R1')
  })
  it('says runs every cycle for Always / no detection', () => {
    expect(describeEntry({ Type: 'CMD', Name: 'x' })).toBe('Runs x; runs every cycle')
    expect(describeEntry({ Type: 'CMD', Name: 'x', DetectionMethod: 'Always' })).toBe('Runs x; runs every cycle')
  })
  it('appends a CMM gate note', () => {
    const text = describeEntry({ Type: 'MSI', Name: 'x', _CmmVersion: '2016' })
    expect(text).toMatch(/CMM 2016 bays only/)
  })
  it('appends the tracked app name when present', () => {
    const text = describeEntry({ Type: 'MSI', Name: 'x', appname: 'PC-DMIS' })
    expect(text).toMatch(/tracked: PC-DMIS/)
  })
  it('falls back to Applies for an unknown type', () => {
    expect(describeEntry({ Type: 'Weird', Name: 'x' })).toMatch(/^Applies x/)
  })
})

describe('detectionMethodHint', () => {
  it('has a non-empty hint for every detection method', () => {
    for (const method of DETECTION_METHODS) {
      expect(detectionMethodHint(method).length).toBeGreaterThan(0)
    }
  })
  it('describes the no-detection case for empty/undefined', () => {
    expect(detectionMethodHint('')).toMatch(/every cycle/)
    expect(detectionMethodHint(undefined)).toMatch(/every cycle/)
  })
  it('ties FileVersion to the compliance panel', () => {
    expect(detectionMethodHint('FileVersion')).toMatch(/Compliance/)
  })
  it('returns empty string for an unknown method', () => {
    expect(detectionMethodHint('Nonsense')).toBe('')
  })
})
