🏡 index : ironmagma/quintodrome.git

import { describe, it, expect } from 'vitest'
import {
  formatTomlValue,
  buildTomlSections,
  configToToml,
  separateAndSortConfigs,
  flattenConfig,
  escapeTomlKey,
} from './aboutUtils'

describe('formatTomlValue', () => {
  it('handles null and undefined values', () => {
    expect(formatTomlValue(null)).toBe('""')
    expect(formatTomlValue(undefined)).toBe('""')
  })

  it('handles boolean values', () => {
    expect(formatTomlValue('true')).toBe('true')
    expect(formatTomlValue('false')).toBe('false')
    expect(formatTomlValue(true)).toBe('true')
    expect(formatTomlValue(false)).toBe('false')
  })

  it('handles integer values', () => {
    expect(formatTomlValue('123')).toBe('123')
    expect(formatTomlValue('-456')).toBe('-456')
    expect(formatTomlValue('0')).toBe('0')
    expect(formatTomlValue(789)).toBe('789')
  })

  it('handles float values', () => {
    expect(formatTomlValue('123.45')).toBe('123.45')
    expect(formatTomlValue('-67.89')).toBe('-67.89')
    expect(formatTomlValue('0.0')).toBe('0.0')
    expect(formatTomlValue(12.34)).toBe('12.34')
  })

  it('handles duration values', () => {
    expect(formatTomlValue('300ms')).toBe('"300ms"')
    expect(formatTomlValue('5s')).toBe('"5s"')
    expect(formatTomlValue('10m')).toBe('"10m"')
    expect(formatTomlValue('2h')).toBe('"2h"')
    expect(formatTomlValue('1.5s')).toBe('"1.5s"')
  })

  it('handles JSON arrays and objects', () => {
    expect(formatTomlValue('["item1", "item2"]')).toBe('[ "item1", "item2" ]')
    expect(formatTomlValue('{"key": "value"}')).toBe('"""{"key": "value"}"""')
  })

  it('formats different types of arrays correctly', () => {
    // String array
    expect(formatTomlValue('["genre", "tcon", "©gen"]')).toBe(
      '[ "genre", "tcon", "©gen" ]',
    )
    // Mixed array with numbers and strings
    expect(formatTomlValue('[42, "test", true]')).toBe('[ 42, "test", true ]')
    // Empty array
    expect(formatTomlValue('[]')).toBe('[ ]')
    // Array with special characters in strings
    expect(
      formatTomlValue('["item with spaces", "item\\"with\\"quotes"]'),
    ).toBe('[ "item with spaces", "item\\"with\\"quotes" ]')
  })

  it('handles invalid JSON as regular strings', () => {
    expect(formatTomlValue('[invalid json')).toBe('"[invalid json"')
    expect(formatTomlValue('{broken')).toBe('"{broken"')
  })

  it('handles regular strings with quote escaping', () => {
    expect(formatTomlValue('simple string')).toBe('"simple string"')
    expect(formatTomlValue('string with "quotes"')).toBe(
      '"string with \\"quotes\\""',
    )
    expect(formatTomlValue('/path/to/file')).toBe('"/path/to/file"')
  })

  it('handles strings with backslashes and quotes', () => {
    expect(formatTomlValue('C:\\Program Files\\app')).toBe(
      '"C:\\\\Program Files\\\\app"',
    )
    expect(formatTomlValue('path\\to"file')).toBe('"path\\\\to\\"file"')
    expect(formatTomlValue('backslash\\ and "quote"')).toBe(
      '"backslash\\\\ and \\"quote\\""',
    )
    expect(formatTomlValue('single\\backslash')).toBe('"single\\\\backslash"')
  })

  it('handles empty strings', () => {
    expect(formatTomlValue('')).toBe('""')
  })
})

describe('buildTomlSections', () => {
  it('separates root keys from nested keys', () => {
    const configs = [
      { key: 'RootKey1', value: 'value1' },
      { key: 'Section.NestedKey', value: 'value2' },
      { key: 'RootKey2', value: 'value3' },
      { key: 'Section.AnotherKey', value: 'value4' },
      { key: 'AnotherSection.Key', value: 'value5' },
    ]

    const result = buildTomlSections(configs)

    expect(result.rootKeys).toEqual([
      { key: 'RootKey1', value: 'value1' },
      { key: 'RootKey2', value: 'value3' },
    ])

    expect(result.sections).toEqual({
      Section: [
        { key: 'NestedKey', value: 'value2' },
        { key: 'AnotherKey', value: 'value4' },
      ],
      AnotherSection: [{ key: 'Key', value: 'value5' }],
    })
  })

  it('handles deeply nested keys', () => {
    const configs = [{ key: 'Section.SubSection.DeepKey', value: 'deepValue' }]

    const result = buildTomlSections(configs)

    expect(result.rootKeys).toEqual([])
    expect(result.sections).toEqual({
      Section: [{ key: 'SubSection.DeepKey', value: 'deepValue' }],
    })
  })

  it('handles empty input', () => {
    const result = buildTomlSections([])

    expect(result.rootKeys).toEqual([])
    expect(result.sections).toEqual({})
  })
})

describe('configToToml', () => {
  const mockTranslate = (key) => {
    const translations = {
      'about.config.devFlagsHeader':
        'Development Flags (subject to change/removal)',
      'about.config.devFlagsComment':
        'These are experimental settings and may be removed in future versions',
    }
    return translations[key] || key
  }

  it('generates TOML with header and timestamp', () => {
    const configData = {
      config: [{ key: 'TestKey', value: 'testValue' }],
    }

    const result = configToToml(configData, mockTranslate)

    expect(result).toContain('# Navidrome Configuration')
    expect(result).toContain('# Generated on')
    expect(result).toContain('TestKey = "testValue"')
  })

  it('separates and sorts regular and dev configs', () => {
    const configData = {
      config: [
        { key: 'ZRegularKey', value: 'regularValue' },
        { key: 'DevTestFlag', value: 'true' },
        { key: 'ARegularKey', value: 'anotherValue' },
        { key: 'DevAnotherFlag', value: 'false' },
      ],
    }

    const result = configToToml(configData, mockTranslate)

    // Check that regular configs come first and are sorted
    const lines = result.split('\n')
    const aRegularIndex = lines.findIndex((line) =>
      line.includes('ARegularKey'),
    )
    const zRegularIndex = lines.findIndex((line) =>
      line.includes('ZRegularKey'),
    )
    const devHeaderIndex = lines.findIndex((line) =>
      line.includes('Development Flags'),
    )
    const devAnotherIndex = lines.findIndex((line) =>
      line.includes('DevAnotherFlag'),
    )
    const devTestIndex = lines.findIndex((line) => line.includes('DevTestFlag'))

    expect(aRegularIndex).toBeLessThan(zRegularIndex)
    expect(zRegularIndex).toBeLessThan(devHeaderIndex)
    expect(devHeaderIndex).toBeLessThan(devAnotherIndex)
    expect(devAnotherIndex).toBeLessThan(devTestIndex)
  })

  it('skips ConfigFile entries', () => {
    const configData = {
      config: [
        { key: 'ConfigFile', value: '/path/to/config.toml' },
        { key: 'TestKey', value: 'testValue' },
      ],
    }

    const result = configToToml(configData, mockTranslate)

    expect(result).not.toContain('ConfigFile =')
    expect(result).toContain('TestKey = "testValue"')
  })

  it('handles sections correctly', () => {
    const configData = {
      config: [
        { key: 'RootKey', value: 'rootValue' },
        { key: 'Section.NestedKey', value: 'nestedValue' },
        { key: 'Section.AnotherKey', value: 'anotherValue' },
        { key: 'DevA', value: 'DevValue' },
      ],
    }

    const result = configToToml(configData, mockTranslate)
    // Fields in a section are sorted alphabetically
    const fields = [
      'RootKey = "rootValue"',
      'DevA = "DevValue"',
      '[Section]',
      'AnotherKey = "anotherValue"',
      'NestedKey = "nestedValue"',
    ]

    for (let idx = 0; idx < fields.length - 1; idx++) {
      expect(result).toContain(fields[idx])

      const idxA = result.indexOf(fields[idx])
      const idxB = result.indexOf(fields[idx + 1])

      expect(idxA).toBeLessThan(idxB)
    }

    expect(result).toContain(fields[fields.length - 1])
  })

  it('includes dev flags header when dev configs exist', () => {
    const configData = {
      config: [
        { key: 'RegularKey', value: 'regularValue' },
        { key: 'DevTestFlag', value: 'true' },
      ],
    }

    const result = configToToml(configData, mockTranslate)

    expect(result).toContain('# Development Flags (subject to change/removal)')
    expect(result).toContain(
      '# These are experimental settings and may be removed in future versions',
    )
    expect(result).toContain('DevTestFlag = true')
  })

  it('does not include dev flags header when no dev configs exist', () => {
    const configData = {
      config: [{ key: 'RegularKey', value: 'regularValue' }],
    }

    const result = configToToml(configData, mockTranslate)

    expect(result).not.toContain('Development Flags')
    expect(result).toContain('RegularKey = "regularValue"')
  })

  it('handles empty config data', () => {
    const configData = { config: [] }

    const result = configToToml(configData, mockTranslate)

    expect(result).toContain('# Navidrome Configuration')
    expect(result).not.toContain('Development Flags')
  })

  it('handles missing config array', () => {
    const configData = {}

    const result = configToToml(configData, mockTranslate)

    expect(result).toContain('# Navidrome Configuration')
    expect(result).not.toContain('Development Flags')
  })

  it('works without translate function', () => {
    const configData = {
      config: [{ key: 'DevTestFlag', value: 'true' }],
    }

    const result = configToToml(configData)

    expect(result).toContain('# about.config.devFlagsHeader')
    expect(result).toContain('# about.config.devFlagsComment')
    expect(result).toContain('DevTestFlag = true')
  })

  it('handles various data types correctly', () => {
    const configData = {
      config: [
        { key: 'StringValue', value: 'test string' },
        { key: 'BooleanValue', value: 'true' },
        { key: 'IntegerValue', value: '42' },
        { key: 'FloatValue', value: '3.14' },
        { key: 'DurationValue', value: '5s' },
        { key: 'ArrayValue', value: '["item1", "item2"]' },
      ],
    }

    const result = configToToml(configData, mockTranslate)

    expect(result).toContain('StringValue = "test string"')
    expect(result).toContain('BooleanValue = true')
    expect(result).toContain('IntegerValue = 42')
    expect(result).toContain('FloatValue = 3.14')
    expect(result).toContain('DurationValue = "5s"')
    expect(result).toContain('ArrayValue = [ "item1", "item2" ]')
  })

  it('handles nested config object format correctly', () => {
    const configData = {
      config: {
        Address: '127.0.0.1',
        Port: 4533,
        EnableDownloads: true,
        DevLogSourceLine: false,
        LastFM: {
          Enabled: true,
          ApiKey: 'secret123',
          Language: 'en',
        },
        Scanner: {
          Schedule: 'daily',
          Enabled: true,
        },
      },
    }

    const result = configToToml(configData, mockTranslate)

    // Should contain regular configs
    expect(result).toContain('Address = "127.0.0.1"')
    expect(result).toContain('Port = 4533')
    expect(result).toContain('EnableDownloads = true')

    // Should contain dev configs with header
    expect(result).toContain('# Development Flags (subject to change/removal)')
    expect(result).toContain('DevLogSourceLine = false')

    // Should contain sections
    expect(result).toContain('[LastFM]')
    expect(result).toContain('Enabled = true')
    expect(result).toContain('ApiKey = "secret123"')
    expect(result).toContain('Language = "en"')

    expect(result).toContain('[Scanner]')
    expect(result).toContain('Schedule = "daily"')
  })

  it('handles mixed nested and flat structure', () => {
    const configData = {
      config: {
        MusicFolder: '/music',
        DevAutoLoginUsername: 'testuser',
        Jukebox: {
          Enabled: false,
          AdminOnly: true,
        },
      },
    }

    const result = configToToml(configData, mockTranslate)

    expect(result).toContain('MusicFolder = "/music"')
    expect(result).toContain('DevAutoLoginUsername = "testuser"')
    expect(result).toContain('[Jukebox]')
    expect(result).toContain('Enabled = false')
    expect(result).toContain('AdminOnly = true')
  })

  it('properly escapes keys with special characters in sections', () => {
    const configData = {
      config: [
        { key: 'DevLogLevels.persistence/sql_base_repository', value: 'trace' },
        { key: 'DevLogLevels.core/scanner', value: 'debug' },
        { key: 'DevLogLevels.regular_key', value: 'info' },
        { key: 'Tags.genre.Aliases', value: '["tcon","genre","©gen"]' },
      ],
    }

    const result = configToToml(configData, mockTranslate)

    // Keys with forward slashes should be quoted
    expect(result).toContain('"persistence/sql_base_repository" = "trace"')
    expect(result).toContain('"core/scanner" = "debug"')

    // Regular keys should not be quoted
    expect(result).toContain('regular_key = "info"')

    // Arrays should be formatted correctly
    expect(result).toContain('"genre.Aliases" = [ "tcon", "genre", "©gen" ]')

    // Should contain proper sections
    expect(result).toContain('[DevLogLevels]')
    expect(result).toContain('[Tags]')
  })
})

describe('flattenConfig', () => {
  it('flattens simple nested objects correctly', () => {
    const config = {
      Address: '0.0.0.0',
      Port: 4533,
      EnableDownloads: true,
      LastFM: {
        Enabled: true,
        ApiKey: 'secret123',
        Language: 'en',
      },
    }

    const result = flattenConfig(config)

    expect(result).toContainEqual({
      key: 'Address',
      envVar: 'ND_ADDRESS',
      value: '0.0.0.0',
    })

    expect(result).toContainEqual({
      key: 'Port',
      envVar: 'ND_PORT',
      value: '4533',
    })

    expect(result).toContainEqual({
      key: 'EnableDownloads',
      envVar: 'ND_ENABLEDOWNLOADS',
      value: 'true',
    })

    expect(result).toContainEqual({
      key: 'LastFM.Enabled',
      envVar: 'ND_LASTFM_ENABLED',
      value: 'true',
    })

    expect(result).toContainEqual({
      key: 'LastFM.ApiKey',
      envVar: 'ND_LASTFM_APIKEY',
      value: 'secret123',
    })

    expect(result).toContainEqual({
      key: 'LastFM.Language',
      envVar: 'ND_LASTFM_LANGUAGE',
      value: 'en',
    })
  })

  it('handles deeply nested objects', () => {
    const config = {
      Scanner: {
        Schedule: 'daily',
        Options: {
          ExtractorType: 'taglib',
          ArtworkPriority: 'cover.jpg',
        },
      },
    }

    const result = flattenConfig(config)

    expect(result).toContainEqual({
      key: 'Scanner.Schedule',
      envVar: 'ND_SCANNER_SCHEDULE',
      value: 'daily',
    })

    expect(result).toContainEqual({
      key: 'Scanner.Options.ExtractorType',
      envVar: 'ND_SCANNER_OPTIONS_EXTRACTORTYPE',
      value: 'taglib',
    })

    expect(result).toContainEqual({
      key: 'Scanner.Options.ArtworkPriority',
      envVar: 'ND_SCANNER_OPTIONS_ARTWORKPRIORITY',
      value: 'cover.jpg',
    })
  })

  it('handles arrays correctly', () => {
    const config = {
      DeviceList: ['device1', 'device2'],
      Settings: {
        EnabledFormats: ['mp3', 'flac', 'ogg'],
      },
    }

    const result = flattenConfig(config)

    expect(result).toContainEqual({
      key: 'DeviceList',
      envVar: 'ND_DEVICELIST',
      value: '["device1","device2"]',
    })

    expect(result).toContainEqual({
      key: 'Settings.EnabledFormats',
      envVar: 'ND_SETTINGS_ENABLEDFORMATS',
      value: '["mp3","flac","ogg"]',
    })
  })

  it('handles null and undefined values', () => {
    const config = {
      NullValue: null,
      UndefinedValue: undefined,
      EmptyString: '',
      ZeroValue: 0,
    }

    const result = flattenConfig(config)

    expect(result).toContainEqual({
      key: 'NullValue',
      envVar: 'ND_NULLVALUE',
      value: 'null',
    })

    expect(result).toContainEqual({
      key: 'UndefinedValue',
      envVar: 'ND_UNDEFINEDVALUE',
      value: 'undefined',
    })

    expect(result).toContainEqual({
      key: 'EmptyString',
      envVar: 'ND_EMPTYSTRING',
      value: '',
    })

    expect(result).toContainEqual({
      key: 'ZeroValue',
      envVar: 'ND_ZEROVALUE',
      value: '0',
    })
  })

  it('handles empty object', () => {
    const result = flattenConfig({})
    expect(result).toEqual([])
  })

  it('handles null/undefined input', () => {
    expect(flattenConfig(null)).toEqual([])
    expect(flattenConfig(undefined)).toEqual([])
  })

  it('handles non-object input', () => {
    expect(flattenConfig('string')).toEqual([])
    expect(flattenConfig(123)).toEqual([])
    expect(flattenConfig(true)).toEqual([])
  })
})

describe('separateAndSortConfigs', () => {
  it('separates regular and dev configs correctly with array input', () => {
    const configs = [
      { key: 'RegularKey1', value: 'value1' },
      { key: 'DevTestFlag', value: 'true' },
      { key: 'AnotherRegular', value: 'value2' },
      { key: 'DevAnotherFlag', value: 'false' },
    ]

    const result = separateAndSortConfigs(configs)

    expect(result.regularConfigs).toEqual([
      { key: 'AnotherRegular', value: 'value2' },
      { key: 'RegularKey1', value: 'value1' },
    ])

    expect(result.devConfigs).toEqual([
      { key: 'DevAnotherFlag', value: 'false' },
      { key: 'DevTestFlag', value: 'true' },
    ])
  })

  it('separates regular and dev configs correctly with nested object input', () => {
    const config = {
      Address: '127.0.0.1',
      Port: 4533,
      DevAutoLoginUsername: 'testuser',
      DevLogSourceLine: true,
      LastFM: {
        Enabled: true,
        ApiKey: 'secret123',
      },
    }

    const result = separateAndSortConfigs(config)

    expect(result.regularConfigs).toEqual([
      { key: 'Address', envVar: 'ND_ADDRESS', value: '127.0.0.1' },
      { key: 'LastFM.ApiKey', envVar: 'ND_LASTFM_APIKEY', value: 'secret123' },
      { key: 'LastFM.Enabled', envVar: 'ND_LASTFM_ENABLED', value: 'true' },
      { key: 'Port', envVar: 'ND_PORT', value: '4533' },
    ])

    expect(result.devConfigs).toEqual([
      {
        key: 'DevAutoLoginUsername',
        envVar: 'ND_DEVAUTOLOGINUSERNAME',
        value: 'testuser',
      },
      { key: 'DevLogSourceLine', envVar: 'ND_DEVLOGSOURCELINE', value: 'true' },
    ])
  })

  it('skips ConfigFile entries', () => {
    const configs = [
      { key: 'ConfigFile', value: '/path/to/config.toml' },
      { key: 'RegularKey', value: 'value' },
      { key: 'DevFlag', value: 'true' },
    ]

    const result = separateAndSortConfigs(configs)

    expect(result.regularConfigs).toEqual([
      { key: 'RegularKey', value: 'value' },
    ])
    expect(result.devConfigs).toEqual([{ key: 'DevFlag', value: 'true' }])
  })

  it('skips ConfigFile entries with nested object input', () => {
    const config = {
      ConfigFile: '/path/to/config.toml',
      RegularKey: 'value',
      DevFlag: true,
    }

    const result = separateAndSortConfigs(config)

    expect(result.regularConfigs).toEqual([
      { key: 'RegularKey', envVar: 'ND_REGULARKEY', value: 'value' },
    ])
    expect(result.devConfigs).toEqual([
      { key: 'DevFlag', envVar: 'ND_DEVFLAG', value: 'true' },
    ])
  })

  it('handles empty input', () => {
    const result = separateAndSortConfigs([])

    expect(result.regularConfigs).toEqual([])
    expect(result.devConfigs).toEqual([])
  })

  it('handles null/undefined input', () => {
    const result1 = separateAndSortConfigs(null)
    const result2 = separateAndSortConfigs(undefined)

    expect(result1.regularConfigs).toEqual([])
    expect(result1.devConfigs).toEqual([])
    expect(result2.regularConfigs).toEqual([])
    expect(result2.devConfigs).toEqual([])
  })

  it('sorts configs alphabetically', () => {
    const configs = [
      { key: 'ZRegular', value: 'z' },
      { key: 'ARegular', value: 'a' },
      { key: 'DevZ', value: 'z' },
      { key: 'DevA', value: 'a' },
    ]

    const result = separateAndSortConfigs(configs)

    expect(result.regularConfigs[0].key).toBe('ARegular')
    expect(result.regularConfigs[1].key).toBe('ZRegular')
    expect(result.devConfigs[0].key).toBe('DevA')
    expect(result.devConfigs[1].key).toBe('DevZ')
  })
})

describe('escapeTomlKey', () => {
  it('does not escape valid bare keys', () => {
    expect(escapeTomlKey('RegularKey')).toBe('RegularKey')
    expect(escapeTomlKey('regular_key')).toBe('regular_key')
    expect(escapeTomlKey('regular-key')).toBe('regular-key')
    expect(escapeTomlKey('key123')).toBe('key123')
    expect(escapeTomlKey('Key_with_underscores')).toBe('Key_with_underscores')
    expect(escapeTomlKey('Key-with-hyphens')).toBe('Key-with-hyphens')
  })

  it('escapes keys with special characters', () => {
    // Keys with forward slashes (like DevLogLevels keys)
    expect(escapeTomlKey('persistence/sql_base_repository')).toBe(
      '"persistence/sql_base_repository"',
    )
    expect(escapeTomlKey('core/scanner')).toBe('"core/scanner"')

    // Keys with dots
    expect(escapeTomlKey('Section.NestedKey')).toBe('"Section.NestedKey"')

    // Keys with spaces
    expect(escapeTomlKey('key with spaces')).toBe('"key with spaces"')

    // Keys with other special characters
    expect(escapeTomlKey('key@with@symbols')).toBe('"key@with@symbols"')
    expect(escapeTomlKey('key+with+plus')).toBe('"key+with+plus"')
  })

  it('escapes quotes in keys', () => {
    expect(escapeTomlKey('key"with"quotes')).toBe('"key\\"with\\"quotes"')
    expect(escapeTomlKey('key with "quotes" inside')).toBe(
      '"key with \\"quotes\\" inside"',
    )
  })

  it('escapes backslashes in keys', () => {
    expect(escapeTomlKey('key\\with\\backslashes')).toBe(
      '"key\\\\with\\\\backslashes"',
    )
    expect(escapeTomlKey('path\\to\\file')).toBe('"path\\\\to\\\\file"')
  })

  it('handles empty and null keys', () => {
    expect(escapeTomlKey('')).toBe('""')
    expect(escapeTomlKey(null)).toBe('null')
    expect(escapeTomlKey(undefined)).toBe('undefined')
  })
})