import { describe, it, expect } from 'vitest'
import {
  isSafeFilenameTemplate,
  isSafeOutputDir,
  isValidHistoryEntry,
  isValidErrorLogEntry,
  isTemplateLike,
  isValidSource,
  isValidMediaItem
} from '../src/main/core/validation'
import {
  isValidSyncTime,
  type HistoryEntry,
  type ErrorLogEntry,
  type Source,
  type MediaItem
} from '@shared/ipc'

// --- L51: daily-sync time (reaches `schtasks /ST`) ---------------------------

describe('isValidSyncTime', () => {
  it('accepts well-formed 24h HH:MM times', () => {
    expect(isValidSyncTime('00:00')).toBe(true)
    expect(isValidSyncTime('09:00')).toBe(true)
    expect(isValidSyncTime('19:30')).toBe(true)
    expect(isValidSyncTime('23:59')).toBe(true)
  })

  it('rejects out-of-range, partial, and 12h forms', () => {
    expect(isValidSyncTime('24:00')).toBe(false)
    expect(isValidSyncTime('12:60')).toBe(false)
    expect(isValidSyncTime('9:00')).toBe(false) // must be zero-padded
    expect(isValidSyncTime('09:0')).toBe(false)
    expect(isValidSyncTime('9am')).toBe(false)
    expect(isValidSyncTime('')).toBe(false)
  })

  it('rejects non-strings and injection-shaped values', () => {
    expect(isValidSyncTime(undefined)).toBe(false)
    expect(isValidSyncTime(900)).toBe(false)
    expect(isValidSyncTime('09:00 /TN evil')).toBe(false) // nothing may trail the time
    expect(isValidSyncTime('09:00\n')).toBe(false)
  })
})

// --- S4: filename template path-traversal -----------------------------------

describe('isSafeFilenameTemplate', () => {
  it('accepts the default template', () => {
    expect(isSafeFilenameTemplate('%(title)s.%(ext)s')).toBe(true)
  })

  it('accepts sub-folder templates with yt-dlp fields', () => {
    expect(isSafeFilenameTemplate('%(uploader)s/%(title)s.%(ext)s')).toBe(true)
    expect(isSafeFilenameTemplate('%(uploader)s\\%(title)s.%(ext)s')).toBe(true)
  })

  it('rejects forward-slash traversal', () => {
    expect(isSafeFilenameTemplate('%(title)s/../../evil.%(ext)s')).toBe(false)
  })

  it('rejects backslash traversal', () => {
    expect(isSafeFilenameTemplate('%(title)s\\..\\..\\win32.exe')).toBe(false)
  })

  it('rejects a bare .. segment', () => {
    expect(isSafeFilenameTemplate('..')).toBe(false)
  })

  it('rejects absolute paths', () => {
    expect(isSafeFilenameTemplate('C:\\Windows\\system32\\%(title)s')).toBe(false)
    expect(isSafeFilenameTemplate('/etc/%(title)s')).toBe(false)
  })

  it('allows .. only when embedded in a longer segment (not a real traversal)', () => {
    // '..foo' / 'foo..bar' are filenames, not parent-dir references.
    expect(isSafeFilenameTemplate('my..video.%(ext)s')).toBe(true)
    expect(isSafeFilenameTemplate('.%(title)s.%(ext)s')).toBe(true) // leading dot = hidden file
  })

  // --- T1: Windows-specific bypasses --------------------------------------

  it('rejects a drive-relative prefix that path.isAbsolute misses', () => {
    // 'C:foo' is NOT absolute per Node, but still escapes the output dir.
    expect(isSafeFilenameTemplate('C:foo\\%(title)s.%(ext)s')).toBe(false)
    expect(isSafeFilenameTemplate('c:%(title)s')).toBe(false)
  })

  it('rejects a .. segment dressed up with trailing/leading dots or spaces', () => {
    // Windows trims trailing dots/spaces, so these all resolve to '..'.
    expect(isSafeFilenameTemplate('%(title)s/.. /x')).toBe(false)
    expect(isSafeFilenameTemplate('%(title)s/.. ./x')).toBe(false)
    expect(isSafeFilenameTemplate('%(title)s/ ../x')).toBe(false)
    expect(isSafeFilenameTemplate('%(title)s\\..  \\x')).toBe(false)
    expect(isSafeFilenameTemplate('...')).toBe(false)
  })
})

// --- S4: output directory ----------------------------------------------------

describe('isSafeOutputDir', () => {
  it('accepts an empty string (resolves to OS Downloads)', () => {
    expect(isSafeOutputDir('')).toBe(true)
  })

  it('accepts an absolute Windows path', () => {
    expect(isSafeOutputDir('C:\\Users\\me\\Downloads')).toBe(true)
  })

  it('rejects a relative path', () => {
    expect(isSafeOutputDir('Downloads')).toBe(false)
    expect(isSafeOutputDir('..\\elsewhere')).toBe(false)
  })
})

// --- S5: persisted JSON entry validation -------------------------------------

const validHistory: HistoryEntry = {
  id: 'a',
  title: 'A video',
  url: 'https://example.com/v',
  kind: 'video',
  quality: '1080p',
  completedAt: 1700000000000
}

describe('isValidHistoryEntry', () => {
  it('accepts a well-formed entry', () => {
    expect(isValidHistoryEntry(validHistory)).toBe(true)
  })

  it('accepts optional string fields', () => {
    expect(
      isValidHistoryEntry({
        ...validHistory,
        filePath: 'C:\\x.mp4',
        channel: 'ch',
        sizeLabel: '10 MB',
        thumbnail: 'https://x/y.jpg'
      })
    ).toBe(true)
  })

  it('rejects a wrong kind', () => {
    expect(isValidHistoryEntry({ ...validHistory, kind: 'image' })).toBe(false)
  })

  it('rejects a missing required field', () => {
    const { url: _url, ...noUrl } = validHistory
    expect(isValidHistoryEntry(noUrl)).toBe(false)
  })

  it('rejects a non-string filePath (e.g. injected number/object)', () => {
    expect(isValidHistoryEntry({ ...validHistory, filePath: 42 })).toBe(false)
  })

  it('rejects non-objects', () => {
    expect(isValidHistoryEntry(null)).toBe(false)
    expect(isValidHistoryEntry('nope')).toBe(false)
    expect(isValidHistoryEntry(undefined)).toBe(false)
  })
})

const validError: ErrorLogEntry = {
  id: 'e',
  url: 'https://example.com/v',
  kind: 'audio',
  error: 'boom',
  occurredAt: 1700000000000
}

describe('isValidErrorLogEntry', () => {
  it('accepts a well-formed entry', () => {
    expect(isValidErrorLogEntry(validError)).toBe(true)
    expect(isValidErrorLogEntry({ ...validError, title: 'optional' })).toBe(true)
  })

  it('accepts an optional detail string and rejects a non-string one', () => {
    expect(isValidErrorLogEntry({ ...validError, detail: 'the full transcript' })).toBe(true)
    expect(isValidErrorLogEntry({ ...validError, detail: 42 })).toBe(false)
  })

  it('rejects a missing error message', () => {
    const { error: _error, ...noError } = validError
    expect(isValidErrorLogEntry(noError)).toBe(false)
  })

  it('rejects a non-numeric occurredAt', () => {
    expect(isValidErrorLogEntry({ ...validError, occurredAt: 'yesterday' })).toBe(false)
  })
})

describe('isTemplateLike', () => {
  it('accepts an object with a string or numeric id', () => {
    expect(isTemplateLike({ id: 'x', name: 'n', args: '--foo' })).toBe(true)
    expect(isTemplateLike({ id: 1 })).toBe(true)
  })

  it('rejects entries without an id', () => {
    expect(isTemplateLike({ name: 'n', args: '--foo' })).toBe(false)
    expect(isTemplateLike(null)).toBe(false)
    expect(isTemplateLike('str')).toBe(false)
  })
})

// --- T7: source row validation (feedUrl drives a network fetch) --------------

const validSource: Source = {
  id: 's1',
  url: 'https://www.youtube.com/@x',
  kind: 'channel',
  title: 'X',
  addedAt: 1700000000000,
  itemCount: 5
}

describe('isValidSource', () => {
  it('accepts a well-formed source and its optional fields', () => {
    expect(isValidSource(validSource)).toBe(true)
    expect(
      isValidSource({
        ...validSource,
        channel: 'X',
        watched: true,
        feedUrl: 'https://www.youtube.com/feeds/videos.xml?channel_id=UC',
        lastIndexedAt: 1700000000001
      })
    ).toBe(true)
  })

  it('rejects a non-string feedUrl (audit T7)', () => {
    expect(isValidSource({ ...validSource, feedUrl: 123 })).toBe(false)
  })

  it('rejects a non-boolean watched (audit T7)', () => {
    expect(isValidSource({ ...validSource, watched: 'yes' })).toBe(false)
  })

  it('rejects a wrong kind or a missing required field', () => {
    expect(isValidSource({ ...validSource, kind: 'video' })).toBe(false)
    const { id: _id, ...noId } = validSource
    expect(isValidSource(noId)).toBe(false)
  })

  it('rejects non-objects', () => {
    expect(isValidSource(null)).toBe(false)
    expect(isValidSource('nope')).toBe(false)
  })
})

// --- L35: isValidMediaItem — the persisted media-items.json row validator ----

describe('isValidMediaItem', () => {
  const valid: MediaItem = {
    id: 'src1:vid1',
    sourceId: 'src1',
    videoId: 'vid1',
    title: 'A video',
    url: 'https://youtube.com/watch?v=vid1',
    playlistTitle: 'Uploads',
    playlistIndex: 1,
    downloaded: false
  }

  it('accepts a minimal valid item', () => {
    expect(isValidMediaItem(valid)).toBe(true)
  })

  it('accepts the optional fields when well-typed', () => {
    expect(
      isValidMediaItem({
        ...valid,
        durationLabel: '3:21',
        downloaded: true,
        downloadedAt: 1_700_000_000_000,
        filePath: 'C:/Videos/a.mp4'
      })
    ).toBe(true)
  })

  it('rejects a missing or wrong-typed required field', () => {
    const { videoId: _v, ...noVideoId } = valid
    expect(isValidMediaItem(noVideoId)).toBe(false)
    expect(isValidMediaItem({ ...valid, playlistIndex: '1' })).toBe(false)
    expect(isValidMediaItem({ ...valid, downloaded: 'no' })).toBe(false)
  })

  it('rejects wrong-typed optional fields', () => {
    expect(isValidMediaItem({ ...valid, durationLabel: 42 })).toBe(false)
    expect(isValidMediaItem({ ...valid, downloadedAt: 'soon' })).toBe(false)
    expect(isValidMediaItem({ ...valid, filePath: 99 })).toBe(false)
  })

  it('rejects non-objects', () => {
    expect(isValidMediaItem(null)).toBe(false)
    expect(isValidMediaItem('nope')).toBe(false)
  })
})
