/**
 * Config Loader Tests
 */

import { writeFileSync, mkdirSync, rmSync } from 'fs'
import { join } from 'path'
import { tmpdir } from 'os'
import {
  loadSuppressionConfig,
  loadConfigFromPath,
  findConfigFile,
  writeSuppressionConfig,
  addFindingSuppression,
  removeFindingSuppression,
  listSuppressions,
  isExpired,
} from '../config-loader'
import type { SuppressionConfig, FindingSuppression } from '../types'

describe('Config Loader', () => {
  let testDir: string

  beforeEach(() => {
    // Create unique temp directory for each test
    testDir = join(tmpdir(), `oculum-test-${Date.now()}-${Math.random().toString(36).slice(2)}`)
    mkdirSync(testDir, { recursive: true })
  })

  afterEach(() => {
    // Clean up
    try {
      rmSync(testDir, { recursive: true, force: true })
    } catch {
      // Ignore cleanup errors
    }
  })

  describe('findConfigFile', () => {
    it('should find .oculum.yaml in current directory', () => {
      writeFileSync(join(testDir, '.oculum.yaml'), 'version: 1')
      const result = findConfigFile(testDir)
      expect(result).toBe(join(testDir, '.oculum.yaml'))
    })

    it('should find .oculum.yml in current directory', () => {
      writeFileSync(join(testDir, '.oculum.yml'), 'version: 1')
      const result = findConfigFile(testDir)
      expect(result).toBe(join(testDir, '.oculum.yml'))
    })

    it('should find oculum.config.json in current directory', () => {
      writeFileSync(join(testDir, 'oculum.config.json'), '{"version": 1}')
      const result = findConfigFile(testDir)
      expect(result).toBe(join(testDir, 'oculum.config.json'))
    })

    it('should prefer .oculum.yaml over other formats', () => {
      writeFileSync(join(testDir, '.oculum.yaml'), 'version: 1')
      writeFileSync(join(testDir, 'oculum.config.json'), '{"version": 1}')
      const result = findConfigFile(testDir)
      expect(result).toBe(join(testDir, '.oculum.yaml'))
    })

    it('should find config in parent directory', () => {
      const subDir = join(testDir, 'src', 'utils')
      mkdirSync(subDir, { recursive: true })
      writeFileSync(join(testDir, '.oculum.yaml'), 'version: 1')

      const result = findConfigFile(subDir)
      expect(result).toBe(join(testDir, '.oculum.yaml'))
    })

    it('should return null if no config found', () => {
      const result = findConfigFile(testDir)
      expect(result).toBeNull()
    })
  })

  describe('loadConfigFromPath', () => {
    it('should load valid YAML config', () => {
      const configPath = join(testDir, '.oculum.yaml')
      const configContent = `
version: 1
suppressions:
  rules:
    - category: high_entropy_string
      reason: false positives
  findings:
    - hash: "0123456789abcdef"
      file: "src/config.ts"
      reason: static config
ignore:
  - "**/*.test.ts"
`
      writeFileSync(configPath, configContent)

      const result = loadConfigFromPath(configPath)

      expect(result.found).toBe(true)
      expect(result.errors).toHaveLength(0)
      expect(result.config.version).toBe(1)
      expect(result.config.suppressions?.rules).toHaveLength(1)
      expect(result.config.suppressions?.findings).toHaveLength(1)
      expect(result.config.ignore).toHaveLength(1)
    })

    it('should load valid JSON config', () => {
      const configPath = join(testDir, 'oculum.config.json')
      const config: SuppressionConfig = {
        version: 1,
        suppressions: {
          rules: [{
            category: 'hardcoded_secret',
            reason: 'test data',
          }],
        },
      }
      writeFileSync(configPath, JSON.stringify(config))

      const result = loadConfigFromPath(configPath)

      expect(result.found).toBe(true)
      expect(result.errors).toHaveLength(0)
      expect(result.config.suppressions?.rules).toHaveLength(1)
    })

    it('should return errors for invalid YAML', () => {
      const configPath = join(testDir, '.oculum.yaml')
      writeFileSync(configPath, '{ invalid yaml }}}')

      const result = loadConfigFromPath(configPath)

      expect(result.found).toBe(true)
      expect(result.errors.length).toBeGreaterThan(0)
    })

    it('should validate required fields', () => {
      const configPath = join(testDir, '.oculum.yaml')
      const configContent = `
version: 1
suppressions:
  rules:
    - category: high_entropy_string
      # missing reason
  findings:
    - hash: "abc123"
      # missing file and reason
`
      writeFileSync(configPath, configContent)

      const result = loadConfigFromPath(configPath)

      expect(result.errors.length).toBeGreaterThan(0)
      expect(result.errors.some(e => e.includes('reason'))).toBe(true)
    })
  })

  describe('loadSuppressionConfig', () => {
    it('should return default config when no file exists', () => {
      const result = loadSuppressionConfig(testDir)

      expect(result.found).toBe(false)
      expect(result.errors).toHaveLength(0)
      expect(result.config.version).toBe(1)
      expect(result.config.suppressions?.rules).toHaveLength(0)
      expect(result.config.suppressions?.findings).toHaveLength(0)
    })

    it('should find and load config from project path', () => {
      const configPath = join(testDir, '.oculum.yaml')
      writeFileSync(configPath, 'version: 1\nignore:\n  - "*.test.ts"')

      const result = loadSuppressionConfig(testDir)

      expect(result.found).toBe(true)
      expect(result.configPath).toBe(configPath)
      expect(result.config.ignore).toContain('*.test.ts')
    })
  })

  describe('writeSuppressionConfig', () => {
    it('should write YAML config', () => {
      const configPath = join(testDir, '.oculum.yaml')
      const config: SuppressionConfig = {
        version: 1,
        suppressions: {
          findings: [{
            hash: 'abc123def4567890',
            file: 'src/test.ts',
            reason: 'test',
          }],
        },
      }

      writeSuppressionConfig(configPath, config)

      const result = loadConfigFromPath(configPath)
      expect(result.config.suppressions?.findings).toHaveLength(1)
      expect(result.config.suppressions?.findings?.[0].hash).toBe('abc123def4567890')
    })

    it('should write JSON config', () => {
      const configPath = join(testDir, 'oculum.config.json')
      const config: SuppressionConfig = {
        version: 1,
        suppressions: {
          rules: [{
            category: 'hardcoded_secret',
            reason: 'test',
          }],
        },
      }

      writeSuppressionConfig(configPath, config)

      const result = loadConfigFromPath(configPath)
      expect(result.config.suppressions?.rules).toHaveLength(1)
    })
  })

  describe('addFindingSuppression', () => {
    it('should create config file if none exists', () => {
      const suppression: FindingSuppression = {
        hash: 'abc123def4567890',
        file: 'src/test.ts',
        reason: 'test suppression',
      }

      const result = addFindingSuppression(testDir, suppression)

      expect(result.success).toBe(true)
      expect(result.configPath).toBe(join(testDir, '.oculum.yaml'))

      const loaded = loadSuppressionConfig(testDir)
      expect(loaded.config.suppressions?.findings).toHaveLength(1)
      expect(loaded.config.suppressions?.findings?.[0].hash).toBe('abc123def4567890')
    })

    it('should add to existing config', () => {
      // Create initial config
      writeFileSync(join(testDir, '.oculum.yaml'), `
version: 1
suppressions:
  findings:
    - hash: existing123456789
      file: existing.ts
      reason: existing
`)

      const suppression: FindingSuppression = {
        hash: 'newone123456789a',
        file: 'new.ts',
        reason: 'new',
      }

      addFindingSuppression(testDir, suppression)

      const loaded = loadSuppressionConfig(testDir)
      expect(loaded.config.suppressions?.findings).toHaveLength(2)
    })

    it('should update existing suppression with same hash', () => {
      const suppression1: FindingSuppression = {
        hash: 'abc123def4567890',
        file: 'src/test.ts',
        reason: 'original reason',
      }
      addFindingSuppression(testDir, suppression1)

      const suppression2: FindingSuppression = {
        hash: 'abc123def4567890',
        file: 'src/test.ts',
        reason: 'updated reason',
      }
      addFindingSuppression(testDir, suppression2)

      const loaded = loadSuppressionConfig(testDir)
      expect(loaded.config.suppressions?.findings).toHaveLength(1)
      expect(loaded.config.suppressions?.findings?.[0].reason).toBe('updated reason')
    })
  })

  describe('removeFindingSuppression', () => {
    it('should remove existing suppression', () => {
      // Create config with suppression
      const suppression: FindingSuppression = {
        hash: 'abc123def4567890',
        file: 'src/test.ts',
        reason: 'test',
      }
      addFindingSuppression(testDir, suppression)

      const result = removeFindingSuppression(testDir, 'abc123def4567890')

      expect(result.success).toBe(true)
      expect(result.removed).toBe(true)

      const loaded = loadSuppressionConfig(testDir)
      expect(loaded.config.suppressions?.findings).toHaveLength(0)
    })

    it('should return removed=false for non-existent hash', () => {
      // Create config without the hash
      writeFileSync(join(testDir, '.oculum.yaml'), 'version: 1')

      const result = removeFindingSuppression(testDir, 'nonexistent1234567')

      expect(result.success).toBe(true)
      expect(result.removed).toBe(false)
    })

    it('should return success if no config file exists', () => {
      const result = removeFindingSuppression(testDir, 'abc123def4567890')

      expect(result.success).toBe(true)
      expect(result.removed).toBe(false)
    })
  })

  describe('listSuppressions', () => {
    it('should return valid response when config file is not found locally', () => {
      // Create a deeply nested path to minimize chance of finding parent config
      const deepDir = join(testDir, 'a', 'b', 'c', 'd', Date.now().toString())
      mkdirSync(deepDir, { recursive: true })

      const result = listSuppressions(deepDir)

      // Should return arrays (possibly empty, possibly from a parent config)
      expect(Array.isArray(result.rules)).toBe(true)
      expect(Array.isArray(result.findings)).toBe(true)
      expect(result.errors).toHaveLength(0)
    })

    it('should return all suppressions from config', () => {
      writeFileSync(join(testDir, '.oculum.yaml'), `
version: 1
suppressions:
  rules:
    - category: hardcoded_secret
      reason: test data
  findings:
    - hash: abc123def4567890
      file: test.ts
      reason: suppressed
`)

      const result = listSuppressions(testDir)

      expect(result.rules).toHaveLength(1)
      expect(result.findings).toHaveLength(1)
      expect(result.configPath).toBe(join(testDir, '.oculum.yaml'))
    })
  })

  describe('isExpired', () => {
    it('should return false for undefined date', () => {
      expect(isExpired(undefined)).toBe(false)
    })

    it('should return true for past date', () => {
      expect(isExpired('2020-01-01')).toBe(true)
    })

    it('should return false for future date', () => {
      expect(isExpired('2030-01-01')).toBe(false)
    })

    it('should handle ISO date strings', () => {
      const futureDate = new Date()
      futureDate.setFullYear(futureDate.getFullYear() + 1)
      expect(isExpired(futureDate.toISOString())).toBe(false)

      const pastDate = new Date()
      pastDate.setFullYear(pastDate.getFullYear() - 1)
      expect(isExpired(pastDate.toISOString())).toBe(true)
    })

    it('should return false for invalid date strings', () => {
      expect(isExpired('not-a-date')).toBe(false)
    })
  })
})
