import { describe, it, expect, beforeEach, vi } from 'vitest'
import { prismaMock, resetPrismaMock } from '../../../mocks/prisma'
import crypto from 'crypto'

// Import after prisma mock is set up
import { apiKeyService } from '@/lib/services/api-key-service'

describe('ApiKeyService', () => {
  beforeEach(() => {
    resetPrismaMock()
  })

  describe('generateKey', () => {
    it('should generate a key with hk_ prefix', () => {
      const key = apiKeyService.generateKey()

      expect(key).toMatch(/^hk_/)
    })

    it('should generate a 68-character key (hk_ + 64 hex chars)', () => {
      const key = apiKeyService.generateKey()

      // "hk_" (3 chars) + 32 bytes hex (64 chars) = 67 chars
      expect(key).toHaveLength(67)
    })

    it('should generate unique keys each time', () => {
      const key1 = apiKeyService.generateKey()
      const key2 = apiKeyService.generateKey()

      expect(key1).not.toBe(key2)
    })

    it('should generate key with valid hex after prefix', () => {
      const key = apiKeyService.generateKey()
      const hexPart = key.slice(3)

      expect(hexPart).toMatch(/^[0-9a-f]+$/)
    })
  })

  describe('hashKey', () => {
    it('should return a SHA-256 hex hash', () => {
      const key = 'hk_test-api-key'
      const hash = apiKeyService.hashKey(key)

      // SHA-256 produces 64 hex chars
      expect(hash).toHaveLength(64)
      expect(hash).toMatch(/^[0-9a-f]+$/)
    })

    it('should produce consistent hashes for the same input', () => {
      const key = 'hk_same-key'

      const hash1 = apiKeyService.hashKey(key)
      const hash2 = apiKeyService.hashKey(key)

      expect(hash1).toBe(hash2)
    })

    it('should produce different hashes for different inputs', () => {
      const hash1 = apiKeyService.hashKey('hk_key-one')
      const hash2 = apiKeyService.hashKey('hk_key-two')

      expect(hash1).not.toBe(hash2)
    })

    it('should match Node.js crypto SHA-256 output', () => {
      const key = 'hk_verification-key'
      const expectedHash = crypto.createHash('sha256').update(key).digest('hex')

      const hash = apiKeyService.hashKey(key)

      expect(hash).toBe(expectedHash)
    })
  })

  describe('findByApiKey', () => {
    const mockConnection = {
      id: 'conn-123',
      hubApiKey: 'hk_plain-key',
      hubApiKeyHash: null,
      orderId: 'order-456',
      order: {
        id: 'order-456',
        domain: 'test.letsbe.cloud',
        serverIp: '192.168.1.100',
      },
    }

    it('should find connection by hash (preferred path)', async () => {
      const apiKey = 'hk_test-key'
      const hash = crypto.createHash('sha256').update(apiKey).digest('hex')

      prismaMock.serverConnection.findUnique.mockResolvedValueOnce({
        ...mockConnection,
        hubApiKeyHash: hash,
      } as any)

      const result = await apiKeyService.findByApiKey(apiKey)

      expect(result).toBeDefined()
      // First call should be hash lookup
      expect(prismaMock.serverConnection.findUnique).toHaveBeenCalledWith({
        where: { hubApiKeyHash: hash },
        include: {
          order: {
            select: {
              id: true,
              domain: true,
              serverIp: true,
            },
          },
        },
      })
    })

    it('should fallback to plaintext lookup when hash not found', async () => {
      const apiKey = 'hk_legacy-key'

      // First call (hash lookup) returns null
      prismaMock.serverConnection.findUnique.mockResolvedValueOnce(null)
      // Second call (plaintext lookup) returns the connection
      prismaMock.serverConnection.findUnique.mockResolvedValueOnce({
        ...mockConnection,
        hubApiKey: apiKey,
        hubApiKeyHash: null,
      } as any)
      prismaMock.serverConnection.update.mockResolvedValue({} as any)

      const result = await apiKeyService.findByApiKey(apiKey)

      expect(result).toBeDefined()
      // Should have been called twice: hash lookup, then plaintext
      expect(prismaMock.serverConnection.findUnique).toHaveBeenCalledTimes(2)
      expect(prismaMock.serverConnection.findUnique).toHaveBeenNthCalledWith(2, {
        where: { hubApiKey: apiKey },
        include: expect.any(Object),
      })
    })

    it('should migrate plaintext key to hash when found via fallback', async () => {
      const apiKey = 'hk_migrate-me'
      const expectedHash = crypto.createHash('sha256').update(apiKey).digest('hex')

      // Hash lookup fails
      prismaMock.serverConnection.findUnique.mockResolvedValueOnce(null)
      // Plaintext lookup succeeds with no hash
      prismaMock.serverConnection.findUnique.mockResolvedValueOnce({
        ...mockConnection,
        id: 'conn-migrate',
        hubApiKey: apiKey,
        hubApiKeyHash: null,
      } as any)
      prismaMock.serverConnection.update.mockResolvedValue({} as any)

      await apiKeyService.findByApiKey(apiKey)

      // Should have triggered migration update
      expect(prismaMock.serverConnection.update).toHaveBeenCalledWith({
        where: { id: 'conn-migrate' },
        data: { hubApiKeyHash: expectedHash },
      })
    })

    it('should not migrate if hash already exists', async () => {
      const apiKey = 'hk_already-migrated'
      const hash = crypto.createHash('sha256').update(apiKey).digest('hex')

      // Hash lookup fails
      prismaMock.serverConnection.findUnique.mockResolvedValueOnce(null)
      // Plaintext lookup succeeds but hash already set
      prismaMock.serverConnection.findUnique.mockResolvedValueOnce({
        ...mockConnection,
        hubApiKey: apiKey,
        hubApiKeyHash: hash,
      } as any)

      await apiKeyService.findByApiKey(apiKey)

      // Should NOT have triggered migration
      expect(prismaMock.serverConnection.update).not.toHaveBeenCalled()
    })

    it('should return null when key not found by either method', async () => {
      prismaMock.serverConnection.findUnique.mockResolvedValue(null)

      const result = await apiKeyService.findByApiKey('hk_nonexistent')

      expect(result).toBeNull()
    })
  })
})
