/**
 * Unit tests for scan-projects.ts
 */

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { ProjectScanner, ProjectInfo } from '../scan-projects'
import { z } from 'zod'

// Mock dependencies
vi.mock('fs/promises')
vi.mock('fs')
vi.mock('child_process')
vi.mock('cli-progress')
vi.mock('dotenv')

describe('ProjectScanner', () => {
  const mockOptions = {
    projectsDir: '/test/projects',
    dryRun: false,
    skipExisting: false,
    apiUrl: 'http://localhost:4000/graphql',
    email: 'test@example.com',
    password: 'password123',
    outputFormat: 'text' as const,
    verbose: false,
    batchSize: 1,
  }

  describe('validateProjectName', () => {
    it('should validate and sanitize valid project names', () => {
      const scanner = new ProjectScanner(mockOptions)
      const testCases = [
        { input: 'my-project', expected: 'my-project' },
        { input: 'my_project', expected: 'my_project' },
        { input: 'myProject123', expected: 'myProject123' },
        { input: 'my-project_123', expected: 'my-project_123' },
      ]

      testCases.forEach(({ input, expected }) => {
        const result = (scanner as any).validateProjectName(input)
        expect(result).toBe(expected)
      })
    })

    it('should sanitize invalid characters', () => {
      const scanner = new ProjectScanner(mockOptions)
      const testCases = [
        { input: 'my project', expected: 'my-project' },
        { input: 'my@project', expected: 'my-project' },
        { input: 'my.project', expected: 'my-project' },
        { input: 'my---project', expected: 'my-project' },
        { input: '-my-project-', expected: 'my-project' },
      ]

      testCases.forEach(({ input, expected }) => {
        const result = (scanner as any).validateProjectName(input)
        expect(result).toBe(expected)
      })
    })

    it('should truncate long names', () => {
      const scanner = new ProjectScanner(mockOptions)
      const longName = 'a'.repeat(300)
      const result = (scanner as any).validateProjectName(longName)
      expect(result.length).toBeLessThanOrEqual(255)
    })

    it('should handle empty names', () => {
      const scanner = new ProjectScanner(mockOptions)
      const result = (scanner as any).validateProjectName('')
      expect(result).toBe('project')
    })
  })

  describe('sanitizeMetadata', () => {
    it('should sanitize metadata and remove invalid types', () => {
      const scanner = new ProjectScanner(mockOptions)
      const metadata = {
        string: 'value',
        number: 123,
        boolean: true,
        null: null,
        array: [1, 2, 3],
        object: { nested: 'value' },
        function: () => {}, // Should be removed
        undefined: undefined, // Should be removed
      }

      const result = (scanner as any).sanitizeMetadata(metadata)
      expect(result.string).toBe('value')
      expect(result.number).toBe(123)
      expect(result.boolean).toBe(true)
      expect(result.null).toBeNull()
      expect(Array.isArray(result.array)).toBe(true)
      expect(typeof result.object).toBe('object')
      expect(result.function).toBeUndefined()
      expect(result.undefined).toBeUndefined()
    })

    it('should enforce size limits', () => {
      const scanner = new ProjectScanner(mockOptions)
      const largeMetadata = {
        description: 'x'.repeat(15000), // Too large
        version: '1.0.0',
      }

      expect(() => {
        (scanner as any).sanitizeMetadata(largeMetadata)
      }).toThrow('Metadata too large')
    })
  })

  describe('extractDomainFromGitRemote', () => {
    it('should extract domain from HTTPS URLs', () => {
      const scanner = new ProjectScanner(mockOptions)
      const testCases = [
        {
          input: 'https://github.com/user/repo.git',
          expected: 'github-com.dev',
        },
        {
          input: 'https://gitlab.com/user/repo.git',
          expected: 'gitlab-com.dev',
        },
        {
          input: 'https://bitbucket.org/user/repo.git',
          expected: 'bitbucket-org.dev',
        },
      ]

      testCases.forEach(({ input, expected }) => {
        const result = (scanner as any).extractDomainFromGitRemote(input)
        expect(result).toBe(expected)
      })
    })

    it('should extract domain from SSH URLs', () => {
      const scanner = new ProjectScanner(mockOptions)
      const testCases = [
        {
          input: 'git@github.com:user/repo.git',
          expected: 'github-com.dev',
        },
        {
          input: 'ssh://git@gitlab.com:user/repo.git',
          expected: 'gitlab-com.dev',
        },
      ]

      testCases.forEach(({ input, expected }) => {
        const result = (scanner as any).extractDomainFromGitRemote(input)
        expect(result).toBe(expected)
      })
    })

    it('should return undefined for invalid URLs', () => {
      const scanner = new ProjectScanner(mockOptions)
      const result = (scanner as any).extractDomainFromGitRemote('invalid-url')
      expect(result).toBeUndefined()
    })

    it('should return undefined for empty input', () => {
      const scanner = new ProjectScanner(mockOptions)
      const result = (scanner as any).extractDomainFromGitRemote(undefined)
      expect(result).toBeUndefined()
    })
  })

  describe('detectPackageManager', () => {
    it('should detect pnpm from pnpm-lock.yaml', () => {
      const scanner = new ProjectScanner(mockOptions)
      const { existsSync } = require('fs')
      vi.mocked(existsSync).mockImplementation((path: string) => {
        return path.includes('pnpm-lock.yaml')
      })

      const result = (scanner as any).detectPackageManager('/test/project')
      expect(result).toBe('pnpm')
    })

    it('should detect yarn from yarn.lock', () => {
      const scanner = new ProjectScanner(mockOptions)
      const { existsSync } = require('fs')
      vi.mocked(existsSync).mockImplementation((path: string) => {
        return path.includes('yarn.lock')
      })

      const result = (scanner as any).detectPackageManager('/test/project')
      expect(result).toBe('yarn')
    })

    it('should detect npm from package-lock.json', () => {
      const scanner = new ProjectScanner(mockOptions)
      const { existsSync } = require('fs')
      vi.mocked(existsSync).mockImplementation((path: string) => {
        return path.includes('package-lock.json')
      })

      const result = (scanner as any).detectPackageManager('/test/project')
      expect(result).toBe('npm')
    })

    it('should return unknown if no lock file found', () => {
      const scanner = new ProjectScanner(mockOptions)
      const { existsSync } = require('fs')
      vi.mocked(existsSync).mockReturnValue(false)

      const result = (scanner as any).detectPackageManager('/test/project')
      expect(result).toBe('unknown')
    })
  })

  describe('isDuplicate', () => {
    it('should detect duplicates by name', () => {
      const scanner = new ProjectScanner(mockOptions)
      ;(scanner as any).existingTenants = new Map([
        ['my-project', { id: '123', name: 'my-project' }],
      ])

      const project: ProjectInfo = {
        name: 'my-project',
        path: '/test',
        type: 'repo',
        metadata: {},
      }

      const result = (scanner as any).isDuplicate(project)
      expect(result).not.toBeNull()
      expect(result.name).toBe('my-project')
    })

    it('should detect duplicates by domain', () => {
      const scanner = new ProjectScanner(mockOptions)
      ;(scanner as any).existingTenants = new Map([
        ['github-com.dev', { id: '123', name: 'existing', domain: 'github-com.dev' }],
      ])

      const project: ProjectInfo = {
        name: 'new-project',
        path: '/test',
        type: 'repo',
        gitRemote: 'https://github.com/user/repo.git',
        metadata: {},
      }

      const result = (scanner as any).isDuplicate(project)
      expect(result).not.toBeNull()
    })

    it('should return null for non-duplicates', () => {
      const scanner = new ProjectScanner(mockOptions)
      ;(scanner as any).existingTenants = new Map()

      const project: ProjectInfo = {
        name: 'new-project',
        path: '/test',
        type: 'repo',
        metadata: {},
      }

      const result = (scanner as any).isDuplicate(project)
      expect(result).toBeNull()
    })
  })
})

describe('Validation Schemas', () => {
  describe('projectNameSchema', () => {
    it('should accept valid project names', () => {
      const validNames = ['my-project', 'my_project', 'myProject123', 'my-project_123']
      validNames.forEach((name) => {
        expect(() => {
          z.string().min(1).max(255).regex(/^[a-zA-Z0-9_-]+$/).parse(name)
        }).not.toThrow()
      })
    })

    it('should reject invalid project names', () => {
      const invalidNames = ['my project', 'my@project', 'my.project', 'my-project!']
      invalidNames.forEach((name) => {
        expect(() => {
          z.string().min(1).max(255).regex(/^[a-zA-Z0-9_-]+$/).parse(name)
        }).toThrow()
      })
    })
  })

  describe('gitUrlSchema', () => {
    it('should accept valid Git URLs', () => {
      const validUrls = [
        'https://github.com/user/repo.git',
        'git@github.com:user/repo.git',
        'ssh://git@gitlab.com:user/repo.git',
      ]
      validUrls.forEach((url) => {
        expect(() => {
          z.string().url().parse(url)
        }).not.toThrow()
      })
    })

    it('should reject invalid URLs', () => {
      const invalidUrls = ['not-a-url', 'http://', 'git@']
      invalidUrls.forEach((url) => {
        expect(() => {
          z.string().url().parse(url)
        }).toThrow()
      })
    })
  })
})

