import type { FilePath, FileUrlString } from '@shared/types/file'
import { describe, expect, it } from 'vitest'

import { fileUrlToPath, isDangerExt, normalizeExt, toFileUrl, toSafeFileUrl } from '../url'

describe('normalizeExt', () => {
  it('normalizes dotted, cased, and boundary-padded extensions', () => {
    expect(normalizeExt('.EXE. ')).toBe('exe')
    expect(normalizeExt(' .EXE')).toBe('exe')
    expect(normalizeExt('\t..EXE')).toBe('exe')
    expect(normalizeExt('Pdf')).toBe('pdf')
    expect(normalizeExt(null)).toBeNull()
    expect(normalizeExt('...')).toBeNull()
  })

  it('returns null for normalized extensions that are not conservative bare suffixes', () => {
    expect(normalizeExt('tar.gz')).toBeNull()
    expect(normalizeExt('ex e')).toBeNull()
    expect(normalizeExt('dir/exe')).toBeNull()
    expect(normalizeExt('dir\\exe')).toBeNull()
    expect(normalizeExt('exe\0')).toBeNull()
    expect(normalizeExt('x'.repeat(256))).toBeNull()
  })
})

describe('isDangerExt', () => {
  it('returns false for null and empty string', () => {
    expect(isDangerExt(null)).toBe(false)
    expect(isDangerExt('')).toBe(false)
  })

  it('matches case-insensitively and normalizes legacy dotted extensions', () => {
    expect(isDangerExt('exe')).toBe(true)
    expect(isDangerExt('EXE')).toBe(true)
    expect(isDangerExt('Exe')).toBe(true)
    expect(isDangerExt('.exe')).toBe(true)
    expect(isDangerExt('exe.')).toBe(true)
    expect(isDangerExt('exe ')).toBe(true)
  })

  it('matches every category from the policy list', () => {
    const samples = [
      'sh',
      'exe',
      'bat',
      'cmd',
      'js',
      'py',
      'scpt',
      'msc',
      'inf',
      'application',
      'appref-ms',
      'lnk',
      'app',
      'desktop',
      'appimage',
      'run',
      'iso',
      'img',
      'vhd',
      'vhdx',
      'jar',
      'svg',
      'dmg',
      'pkg'
    ]
    for (const ext of samples) {
      expect(isDangerExt(ext)).toBe(true)
    }
  })

  it('returns false for plain document extensions', () => {
    for (const ext of ['pdf', 'txt', 'md', 'png', 'jpg', 'mp4']) {
      expect(isDangerExt(ext)).toBe(false)
    }
  })
})

describe('toFileUrl', () => {
  it('encodes unix paths with spaces and special chars', () => {
    expect(toFileUrl('/foo/bar baz.pdf' as FilePath)).toBe('file:///foo/bar%20baz.pdf')
    expect(toFileUrl('/foo/a#b.txt' as FilePath)).toBe('file:///foo/a%23b.txt')
    expect(toFileUrl('/foo/a?b.txt' as FilePath)).toBe('file:///foo/a%3Fb.txt')
  })

  it('preserves Windows drive letters unencoded', () => {
    expect(toFileUrl('C:\\foo\\bar baz.pdf' as FilePath)).toBe('file:///C:/foo/bar%20baz.pdf')
    expect(toFileUrl('D:\\folder\\file.txt' as FilePath)).toBe('file:///D:/folder/file.txt')
  })

  it('normalizes backslashes to forward slashes', () => {
    expect(toFileUrl('C:\\a\\b\\c.txt' as FilePath)).toBe('file:///C:/a/b/c.txt')
  })

  it('encodes non-ASCII characters', () => {
    expect(toFileUrl('/foo/中文.pdf' as FilePath)).toBe('file:///foo/%E4%B8%AD%E6%96%87.pdf')
  })
})

describe('fileUrlToPath', () => {
  it('decodes unix file URLs with spaces and non-ASCII characters', () => {
    expect(fileUrlToPath('file:///foo/bar%20baz.pdf')).toBe('/foo/bar baz.pdf')
    expect(fileUrlToPath(new URL('file:///foo/%E4%B8%AD%E6%96%87.pdf'))).toBe('/foo/中文.pdf')
  })

  it('decodes Windows drive file URLs without a POSIX leading slash', () => {
    expect(fileUrlToPath('file:///C:/foo/bar%20baz.pdf')).toBe('C:/foo/bar baz.pdf')
  })

  it('preserves UNC hosts as network paths', () => {
    expect(fileUrlToPath('file://server/share/report%20final.pdf')).toBe('//server/share/report final.pdf')
  })

  it('throws for a non-file: URL', () => {
    expect(() => fileUrlToPath(new URL('https://example.com/foo.pdf'))).toThrow(TypeError)
  })

  it('throws on malformed percent-encoding', () => {
    expect(() => fileUrlToPath('file:///foo/%zz.pdf' as FileUrlString)).toThrow(URIError)
  })
})

describe('toSafeFileUrl', () => {
  it('returns the file URL for non-dangerous extensions', () => {
    expect(toSafeFileUrl('/foo/bar.pdf' as FilePath, 'pdf')).toBe('file:///foo/bar.pdf')
    expect(toSafeFileUrl('/foo/img.png' as FilePath, 'png')).toBe('file:///foo/img.png')
  })

  it('returns the dirname URL for dangerous extensions', () => {
    expect(toSafeFileUrl('/foo/bar/payload.exe' as FilePath, 'exe')).toBe('file:///foo/bar')
    expect(toSafeFileUrl('/foo/bar/payload.exe' as FilePath, '.exe')).toBe('file:///foo/bar')
    expect(toSafeFileUrl('/foo/bar/icon.svg' as FilePath, 'svg')).toBe('file:///foo/bar')
  })

  it('returns the dirname for dangerous extension on Windows paths', () => {
    expect(toSafeFileUrl('C:\\foo\\bar\\payload.exe' as FilePath, 'exe')).toBe('file:///C:/foo/bar')
  })

  it('handles null ext as safe (returns full file URL)', () => {
    expect(toSafeFileUrl('/foo/bar' as FilePath, null)).toBe('file:///foo/bar')
  })

  it('handles mixed separators when computing dirname', () => {
    // Defensive: mixed forward-slash / backslash inputs sometimes appear from
    // legacy IPC paths. The dirname should still pick the right cut point.
    expect(toSafeFileUrl('/a/b\\c.exe' as FilePath, 'exe')).toBe('file:///a/b')
  })

  it('wraps root-level dangerous files (POSIX / Windows drive root)', () => {
    // Regression: when a dangerous file sits directly under the filesystem
    // root, the wrap must still degrade to the parent directory. Returning
    // the original path here would defeat the entire safety contract — the
    // renderer would end up with `file:///payload.exe`, which `<embed>` /
    // `<img src>` can hand to OS file associations.
    expect(toSafeFileUrl('/payload.exe' as FilePath, 'exe')).toBe('file:///')
    expect(toSafeFileUrl('C:\\payload.exe' as FilePath, 'exe')).toBe('file:///C:')
  })
})
