import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
import { mkdirSync, rmSync, writeFileSync } from 'fs'
import { join } from 'path'
import { _resetGlobalCache, loadGitignore } from './gitignore'

const TEST_DIR = '/tmp/toes-gitignore-test'

beforeEach(() => {
  _resetGlobalCache()
  mkdirSync(TEST_DIR, { recursive: true })
})

afterEach(() => {
  rmSync(TEST_DIR, { recursive: true, force: true })
})

describe('loadGitignore', () => {
  describe('with .gitignore file', () => {
    test('should parse simple patterns', () => {
      writeFileSync(join(TEST_DIR, '.gitignore'), 'node_modules\n*.log\ndist/')

      const checker = loadGitignore(TEST_DIR)

      expect(checker.shouldExclude('node_modules')).toBe(true)
      expect(checker.shouldExclude('node_modules/package.json')).toBe(true)
      expect(checker.shouldExclude('test.log')).toBe(true)
      expect(checker.shouldExclude('dist/')).toBe(true)
      expect(checker.shouldExclude('dist/main.js')).toBe(true)
      expect(checker.shouldExclude('src/index.ts')).toBe(false)
    })

    test('should ignore comments and empty lines', () => {
      writeFileSync(
        join(TEST_DIR, '.gitignore'),
        '# Comment\nnode_modules\n\n# Another comment\n*.log\n\n'
      )

      const checker = loadGitignore(TEST_DIR)

      expect(checker.shouldExclude('node_modules')).toBe(true)
      expect(checker.shouldExclude('test.log')).toBe(true)
      expect(checker.shouldExclude('# Comment')).toBe(false)
    })

    test('should handle wildcard patterns', () => {
      writeFileSync(join(TEST_DIR, '.gitignore'), '*.log\n*.tmp\n*.swp')

      const checker = loadGitignore(TEST_DIR)

      expect(checker.shouldExclude('debug.log')).toBe(true)
      expect(checker.shouldExclude('cache.tmp')).toBe(true)
      expect(checker.shouldExclude('vim.swp')).toBe(true)
      expect(checker.shouldExclude('src/file.log')).toBe(true)
      expect(checker.shouldExclude('index.ts')).toBe(false)
    })

    test('should handle glob patterns', () => {
      writeFileSync(join(TEST_DIR, '.gitignore'), '**/*.log\n**/dist\n**/.DS_Store')

      const checker = loadGitignore(TEST_DIR)

      expect(checker.shouldExclude('test.log')).toBe(true)
      expect(checker.shouldExclude('src/test.log')).toBe(true)
      expect(checker.shouldExclude('src/nested/test.log')).toBe(true)
      expect(checker.shouldExclude('dist')).toBe(true)
      expect(checker.shouldExclude('src/dist')).toBe(true)
      expect(checker.shouldExclude('.DS_Store')).toBe(true)
      expect(checker.shouldExclude('src/.DS_Store')).toBe(true)
    })

    test('should handle directory patterns', () => {
      writeFileSync(join(TEST_DIR, '.gitignore'), 'dist/\nbuild/\ntemp')

      const checker = loadGitignore(TEST_DIR)

      expect(checker.shouldExclude('dist/')).toBe(true)
      expect(checker.shouldExclude('dist/main.js')).toBe(true)
      expect(checker.shouldExclude('build/')).toBe(true)
      expect(checker.shouldExclude('build/output.js')).toBe(true)
      expect(checker.shouldExclude('temp')).toBe(true)
      expect(checker.shouldExclude('temp/file.txt')).toBe(true)
    })

    test('should handle nested paths', () => {
      writeFileSync(join(TEST_DIR, '.gitignore'), 'node_modules\n.git\n.env.local')

      const checker = loadGitignore(TEST_DIR)

      expect(checker.shouldExclude('src/node_modules/lib.js')).toBe(true)
      expect(checker.shouldExclude('project/node_modules')).toBe(true)
      expect(checker.shouldExclude('.git/config')).toBe(true)
      expect(checker.shouldExclude('.env.local')).toBe(true)
      expect(checker.shouldExclude('config/.env.local')).toBe(true)
    })

    test('should handle exact filename matches', () => {
      writeFileSync(join(TEST_DIR, '.gitignore'), '.DS_Store\nbun.lockb')

      const checker = loadGitignore(TEST_DIR)

      expect(checker.shouldExclude('.DS_Store')).toBe(true)
      expect(checker.shouldExclude('src/.DS_Store')).toBe(true)
      expect(checker.shouldExclude('bun.lockb')).toBe(true)
      expect(checker.shouldExclude('project/bun.lockb')).toBe(true)
    })
  })

  describe('without .gitignore file (default patterns)', () => {
    test('should use default exclusions', () => {
      const checker = loadGitignore(TEST_DIR)

      expect(checker.shouldExclude('node_modules')).toBe(true)
      expect(checker.shouldExclude('node_modules/pkg/index.js')).toBe(true)
      expect(checker.shouldExclude('.DS_Store')).toBe(true)
      expect(checker.shouldExclude('src/.DS_Store')).toBe(true)
      expect(checker.shouldExclude('debug.log')).toBe(true)
      expect(checker.shouldExclude('dist')).toBe(true)
      expect(checker.shouldExclude('dist/main.js')).toBe(true)
      expect(checker.shouldExclude('build')).toBe(true)
      expect(checker.shouldExclude('.env.local')).toBe(true)
      expect(checker.shouldExclude('.git')).toBe(true)
      expect(checker.shouldExclude('.git/config')).toBe(true)
      expect(checker.shouldExclude('bun.lockb')).toBe(true)
    })

    test('should not exclude normal files', () => {
      const checker = loadGitignore(TEST_DIR)

      expect(checker.shouldExclude('src/index.ts')).toBe(false)
      expect(checker.shouldExclude('package.json')).toBe(false)
      expect(checker.shouldExclude('README.md')).toBe(false)
      expect(checker.shouldExclude('tsconfig.json')).toBe(false)
    })
  })

  describe('edge cases', () => {
    test('should handle empty .gitignore', () => {
      writeFileSync(join(TEST_DIR, '.gitignore'), '')

      const checker = loadGitignore(TEST_DIR)

      expect(checker.shouldExclude('anything.js')).toBe(false)
    })

    test('should handle .gitignore with only comments', () => {
      writeFileSync(join(TEST_DIR, '.gitignore'), '# Comment 1\n# Comment 2')

      const checker = loadGitignore(TEST_DIR)

      expect(checker.shouldExclude('anything.js')).toBe(false)
    })

    test('should handle paths with dots', () => {
      writeFileSync(join(TEST_DIR, '.gitignore'), '*.env.*\n.cache')

      const checker = loadGitignore(TEST_DIR)

      expect(checker.shouldExclude('.env.local')).toBe(true)
      expect(checker.shouldExclude('.env.production')).toBe(true)
      expect(checker.shouldExclude('.cache')).toBe(true)
      expect(checker.shouldExclude('src/.cache')).toBe(true)
    })

    test('should handle patterns with trailing slashes', () => {
      writeFileSync(join(TEST_DIR, '.gitignore'), 'node_modules/\nbuild/')

      const checker = loadGitignore(TEST_DIR)

      expect(checker.shouldExclude('node_modules/')).toBe(true)
      expect(checker.shouldExclude('node_modules/package')).toBe(true)
      expect(checker.shouldExclude('build/')).toBe(true)
      expect(checker.shouldExclude('build/output')).toBe(true)
    })
  })

  describe('global gitignore', () => {
    const GLOBAL_IGNORE = '/tmp/toes-global-gitignore-test'

    beforeEach(() => {
      mkdirSync(GLOBAL_IGNORE, { recursive: true })
    })

    afterEach(() => {
      rmSync(GLOBAL_IGNORE, { recursive: true, force: true })
    })

    test('should include patterns from global gitignore', () => {
      const globalIgnorePath = join(GLOBAL_IGNORE, 'ignore')
      writeFileSync(globalIgnorePath, '*.bak\n.idea/')

      const origSpawnSync = require('child_process').spawnSync
      const spawnSyncMock = mock((cmd: string, args: string[], opts: any) => {
        if (cmd === 'git' && args.includes('core.excludesFile')) {
          return { status: 0, stdout: globalIgnorePath + '\n', stderr: '' }
        }
        return origSpawnSync(cmd, args, opts)
      })
      mock.module('child_process', () => ({ spawnSync: spawnSyncMock }))
      _resetGlobalCache()

      const checker = loadGitignore(TEST_DIR)

      expect(checker.shouldExclude('backup.bak')).toBe(true)
      expect(checker.shouldExclude('src/old.bak')).toBe(true)
      expect(checker.shouldExclude('.idea')).toBe(true)
      expect(checker.shouldExclude('src/index.ts')).toBe(false)

      mock.module('child_process', () => ({ spawnSync: origSpawnSync }))
    })

    test('should merge global and local patterns', () => {
      const globalIgnorePath = join(GLOBAL_IGNORE, 'ignore')
      writeFileSync(globalIgnorePath, '*.bak')
      writeFileSync(join(TEST_DIR, '.gitignore'), '*.log')

      const origSpawnSync = require('child_process').spawnSync
      const spawnSyncMock = mock((cmd: string, args: string[], opts: any) => {
        if (cmd === 'git' && args.includes('core.excludesFile')) {
          return { status: 0, stdout: globalIgnorePath + '\n', stderr: '' }
        }
        return origSpawnSync(cmd, args, opts)
      })
      mock.module('child_process', () => ({ spawnSync: spawnSyncMock }))
      _resetGlobalCache()

      const checker = loadGitignore(TEST_DIR)

      expect(checker.shouldExclude('backup.bak')).toBe(true)
      expect(checker.shouldExclude('debug.log')).toBe(true)
      expect(checker.shouldExclude('src/index.ts')).toBe(false)

      mock.module('child_process', () => ({ spawnSync: origSpawnSync }))
    })
  })
})
