import { describe, it, expect, beforeEach, vi } from 'vitest'
import { prismaMock, resetPrismaMock } from '../../../mocks/prisma'
import { DnsRecordStatus, OrderStatus, LogLevel } from '@prisma/client'

// Mock the dns module
vi.mock('dns', () => {
  const mockResolve4 = vi.fn()
  return {
    default: {
      resolve4: mockResolve4,
    },
    resolve4: mockResolve4,
  }
})

// Import after mocking
import dns from 'dns'
import {
  getSubdomainsForTools,
  verifyDns,
  runDnsVerification,
  skipDnsVerification,
  getDnsStatus,
} from '@/lib/services/dns-service'

// Helper to make dns.resolve4 work as expected with promisify
function mockDnsResolve(mapping: Record<string, string[] | null>) {
  const resolve4 = dns.resolve4 as unknown as ReturnType<typeof vi.fn>
  resolve4.mockImplementation((domain: string, callback: (err: Error | null, addresses?: string[]) => void) => {
    const result = mapping[domain]
    if (result === null || result === undefined) {
      callback(new Error('ENOTFOUND'))
    } else {
      callback(null, result)
    }
  })
}

describe('DNS Service', () => {
  beforeEach(() => {
    resetPrismaMock()
    vi.clearAllMocks()
  })

  describe('getSubdomainsForTools', () => {
    it('should return empty array for core tools with no subdomains', () => {
      const subdomains = getSubdomainsForTools(['core', 'portainer', 'orchestrator'])

      expect(subdomains).toEqual([])
    })

    it('should return correct subdomains for poste', () => {
      const subdomains = getSubdomainsForTools(['poste'])

      expect(subdomains).toContain('mail')
    })

    it('should return correct subdomains for chatwoot', () => {
      const subdomains = getSubdomainsForTools(['chatwoot'])

      expect(subdomains).toContain('support')
      expect(subdomains).toContain('helpdesk')
    })

    it('should return correct subdomains for nextcloud', () => {
      const subdomains = getSubdomainsForTools(['nextcloud'])

      expect(subdomains).toContain('cloud')
      expect(subdomains).toContain('collabora')
      expect(subdomains).toContain('whiteboard')
    })

    it('should deduplicate subdomains across tools', () => {
      const subdomains = getSubdomainsForTools(['nextcloud', 'nextcloud'])

      // Should not have duplicates
      const uniqueSubdomains = [...new Set(subdomains)]
      expect(subdomains.length).toBe(uniqueSubdomains.length)
    })

    it('should combine subdomains from multiple tools', () => {
      const subdomains = getSubdomainsForTools(['poste', 'keycloak', 'n8n'])

      expect(subdomains).toContain('mail')
      expect(subdomains).toContain('auth')
      expect(subdomains).toContain('n8n')
    })

    it('should return sorted subdomains', () => {
      const subdomains = getSubdomainsForTools(['nextcloud', 'poste', 'keycloak'])
      const sorted = [...subdomains].sort()

      expect(subdomains).toEqual(sorted)
    })

    it('should handle unknown tool names gracefully', () => {
      const subdomains = getSubdomainsForTools(['unknown-tool', 'poste'])

      // Should still include known tool subdomains
      expect(subdomains).toContain('mail')
    })

    it('should be case-insensitive for tool names', () => {
      const subdomains = getSubdomainsForTools(['POSTE', 'Keycloak'])

      expect(subdomains).toContain('mail')
      expect(subdomains).toContain('auth')
    })

    it('should return correct subdomains for minio', () => {
      const subdomains = getSubdomainsForTools(['minio'])

      expect(subdomains).toContain('minio')
      expect(subdomains).toContain('s3')
    })

    it('should return correct subdomains for calcom', () => {
      const subdomains = getSubdomainsForTools(['calcom'])

      expect(subdomains).toContain('bookings')
    })

    it('should return empty for tools using root domain (ghost, wordpress)', () => {
      const subdomains = getSubdomainsForTools(['ghost', 'wordpress'])

      expect(subdomains).toEqual([])
    })
  })

  describe('verifyDns', () => {
    it('should mark all subdomains as passed via wildcard when wildcard resolves', async () => {
      // Mock wildcard check - any subdomain resolves to expected IP
      mockDnsResolve({
        // The wildcard test uses a random subdomain pattern
      })

      // Make all DNS lookups resolve to the target IP
      const resolve4 = dns.resolve4 as unknown as ReturnType<typeof vi.fn>
      resolve4.mockImplementation((_domain: string, callback: (err: Error | null, addresses?: string[]) => void) => {
        callback(null, ['10.0.0.1'])
      })

      const result = await verifyDns('example.com', '10.0.0.1', ['poste', 'keycloak'])

      expect(result.wildcardPassed).toBe(true)
      expect(result.allPassed).toBe(true)
      expect(result.records.every((r) => r.status === DnsRecordStatus.SKIPPED)).toBe(true)
    })

    it('should check individual subdomains when wildcard fails', async () => {
      let callCount = 0
      const resolve4 = dns.resolve4 as unknown as ReturnType<typeof vi.fn>
      resolve4.mockImplementation((domain: string, callback: (err: Error | null, addresses?: string[]) => void) => {
        callCount++
        if (callCount === 1) {
          // First call is wildcard check - fail it
          callback(new Error('ENOTFOUND'))
        } else if (domain === 'mail.example.com') {
          callback(null, ['10.0.0.1'])
        } else if (domain === 'auth.example.com') {
          callback(null, ['10.0.0.1'])
        } else {
          callback(new Error('ENOTFOUND'))
        }
      })

      const result = await verifyDns('example.com', '10.0.0.1', ['poste', 'keycloak'])

      expect(result.wildcardPassed).toBe(false)
      expect(result.allPassed).toBe(true)
      expect(result.passedCount).toBe(2)
    })

    it('should detect IP mismatch', async () => {
      let callCount = 0
      const resolve4 = dns.resolve4 as unknown as ReturnType<typeof vi.fn>
      resolve4.mockImplementation((_domain: string, callback: (err: Error | null, addresses?: string[]) => void) => {
        callCount++
        if (callCount === 1) {
          // Wildcard check fails
          callback(new Error('ENOTFOUND'))
        } else {
          // Subdomain resolves to wrong IP
          callback(null, ['10.0.0.99'])
        }
      })

      const result = await verifyDns('example.com', '10.0.0.1', ['poste'])

      expect(result.allPassed).toBe(false)
      const mailRecord = result.records.find((r) => r.subdomain === 'mail')
      expect(mailRecord?.status).toBe(DnsRecordStatus.MISMATCH)
      expect(mailRecord?.resolvedIp).toBe('10.0.0.99')
    })

    it('should handle NOT_FOUND when subdomain has no A record', async () => {
      let callCount = 0
      const resolve4 = dns.resolve4 as unknown as ReturnType<typeof vi.fn>
      resolve4.mockImplementation((_domain: string, callback: (err: Error | null, addresses?: string[]) => void) => {
        callCount++
        if (callCount === 1) {
          // Wildcard check fails
          callback(new Error('ENOTFOUND'))
        } else {
          callback(new Error('ENOTFOUND'))
        }
      })

      const result = await verifyDns('example.com', '10.0.0.1', ['poste'])

      expect(result.allPassed).toBe(false)
      const mailRecord = result.records.find((r) => r.subdomain === 'mail')
      expect(mailRecord?.status).toBe(DnsRecordStatus.NOT_FOUND)
    })

    it('should return correct counts', async () => {
      let callCount = 0
      const resolve4 = dns.resolve4 as unknown as ReturnType<typeof vi.fn>
      resolve4.mockImplementation((domain: string, callback: (err: Error | null, addresses?: string[]) => void) => {
        callCount++
        if (callCount === 1) {
          callback(new Error('ENOTFOUND')) // wildcard
        } else if (domain.startsWith('mail')) {
          callback(null, ['10.0.0.1']) // mail passes
        } else {
          callback(new Error('ENOTFOUND')) // others fail
        }
      })

      const result = await verifyDns('example.com', '10.0.0.1', ['poste', 'keycloak'])

      expect(result.totalSubdomains).toBe(2)
      expect(result.passedCount).toBe(1) // only mail passes
    })

    it('should return empty records for tools with no subdomains', async () => {
      const result = await verifyDns('example.com', '10.0.0.1', ['core', 'portainer'])

      expect(result.totalSubdomains).toBe(0)
      expect(result.allPassed).toBe(true)
      expect(result.records).toEqual([])
    })
  })

  describe('runDnsVerification', () => {
    it('should throw if order not found', async () => {
      prismaMock.order.findUnique.mockResolvedValue(null)

      await expect(runDnsVerification('invalid-id')).rejects.toThrow('Order not found')
    })

    it('should throw if server IP not configured', async () => {
      prismaMock.order.findUnique.mockResolvedValue({
        id: 'order-123',
        serverIp: null,
        domain: 'test.com',
        tools: ['poste'],
        dnsVerification: null,
      } as any)

      await expect(runDnsVerification('order-123')).rejects.toThrow('Server IP not configured')
    })
  })

  describe('skipDnsVerification', () => {
    it('should throw if order not found', async () => {
      prismaMock.order.findUnique.mockResolvedValue(null)

      await expect(skipDnsVerification('invalid-id')).rejects.toThrow('Order not found')
    })

    it('should create new dns verification with manual override', async () => {
      prismaMock.order.findUnique.mockResolvedValue({
        id: 'order-123',
        status: OrderStatus.DNS_PENDING,
        dnsVerification: null,
      } as any)
      prismaMock.dnsVerification.create.mockResolvedValue({} as any)
      prismaMock.provisioningLog.create.mockResolvedValue({} as any)
      prismaMock.order.update.mockResolvedValue({} as any)

      await skipDnsVerification('order-123')

      expect(prismaMock.dnsVerification.create).toHaveBeenCalledWith({
        data: expect.objectContaining({
          orderId: 'order-123',
          manualOverride: true,
          allPassed: true,
          verifiedAt: expect.any(Date),
        }),
      })
    })

    it('should update existing dns verification when one exists', async () => {
      prismaMock.order.findUnique.mockResolvedValue({
        id: 'order-123',
        status: OrderStatus.DNS_PENDING,
        dnsVerification: {
          id: 'dns-456',
        },
      } as any)
      prismaMock.dnsVerification.update.mockResolvedValue({} as any)
      prismaMock.provisioningLog.create.mockResolvedValue({} as any)
      prismaMock.order.update.mockResolvedValue({} as any)

      await skipDnsVerification('order-123')

      expect(prismaMock.dnsVerification.update).toHaveBeenCalledWith({
        where: { id: 'dns-456' },
        data: expect.objectContaining({
          manualOverride: true,
          allPassed: true,
        }),
      })
    })

    it('should update order status to DNS_READY when currently DNS_PENDING', async () => {
      prismaMock.order.findUnique.mockResolvedValue({
        id: 'order-123',
        status: OrderStatus.DNS_PENDING,
        dnsVerification: null,
      } as any)
      prismaMock.dnsVerification.create.mockResolvedValue({} as any)
      prismaMock.provisioningLog.create.mockResolvedValue({} as any)
      prismaMock.order.update.mockResolvedValue({} as any)

      await skipDnsVerification('order-123')

      expect(prismaMock.order.update).toHaveBeenCalledWith({
        where: { id: 'order-123' },
        data: expect.objectContaining({
          status: OrderStatus.DNS_READY,
          dnsVerifiedAt: expect.any(Date),
        }),
      })
    })

    it('should update order status to DNS_READY when currently SERVER_READY', async () => {
      prismaMock.order.findUnique.mockResolvedValue({
        id: 'order-123',
        status: OrderStatus.SERVER_READY,
        dnsVerification: null,
      } as any)
      prismaMock.dnsVerification.create.mockResolvedValue({} as any)
      prismaMock.provisioningLog.create.mockResolvedValue({} as any)
      prismaMock.order.update.mockResolvedValue({} as any)

      await skipDnsVerification('order-123')

      expect(prismaMock.order.update).toHaveBeenCalledWith({
        where: { id: 'order-123' },
        data: expect.objectContaining({
          status: OrderStatus.DNS_READY,
        }),
      })
    })

    it('should not update order status when not in DNS_PENDING or SERVER_READY', async () => {
      prismaMock.order.findUnique.mockResolvedValue({
        id: 'order-123',
        status: OrderStatus.FULFILLED,
        dnsVerification: null,
      } as any)
      prismaMock.dnsVerification.create.mockResolvedValue({} as any)
      prismaMock.provisioningLog.create.mockResolvedValue({} as any)

      await skipDnsVerification('order-123')

      expect(prismaMock.order.update).not.toHaveBeenCalled()
    })

    it('should log the manual override', async () => {
      prismaMock.order.findUnique.mockResolvedValue({
        id: 'order-123',
        status: OrderStatus.DNS_PENDING,
        dnsVerification: null,
      } as any)
      prismaMock.dnsVerification.create.mockResolvedValue({} as any)
      prismaMock.provisioningLog.create.mockResolvedValue({} as any)
      prismaMock.order.update.mockResolvedValue({} as any)

      await skipDnsVerification('order-123')

      expect(prismaMock.provisioningLog.create).toHaveBeenCalledWith({
        data: expect.objectContaining({
          orderId: 'order-123',
          level: LogLevel.WARN,
          message: 'DNS verification skipped via manual override',
          step: 'dns',
        }),
      })
    })
  })

  describe('getDnsStatus', () => {
    it('should throw if order not found', async () => {
      prismaMock.order.findUnique.mockResolvedValue(null)

      await expect(getDnsStatus('invalid-id')).rejects.toThrow('Order not found')
    })

    it('should return status with null verification when none exists', async () => {
      prismaMock.order.findUnique.mockResolvedValue({
        id: 'order-123',
        domain: 'test.letsbe.cloud',
        serverIp: '10.0.0.1',
        status: OrderStatus.DNS_PENDING,
        tools: ['poste'],
        dnsVerification: null,
      } as any)

      const result = await getDnsStatus('order-123')

      expect(result.domain).toBe('test.letsbe.cloud')
      expect(result.serverIp).toBe('10.0.0.1')
      expect(result.verification).toBeNull()
      expect(result.requiredSubdomains).toContain('mail')
    })

    it('should return status with verification data when it exists', async () => {
      const lastChecked = new Date()
      prismaMock.order.findUnique.mockResolvedValue({
        id: 'order-123',
        domain: 'test.letsbe.cloud',
        serverIp: '10.0.0.1',
        status: OrderStatus.DNS_READY,
        tools: ['poste'],
        dnsVerification: {
          id: 'dns-456',
          wildcardPassed: false,
          manualOverride: false,
          allPassed: true,
          totalSubdomains: 1,
          passedCount: 1,
          lastCheckedAt: lastChecked,
          verifiedAt: lastChecked,
          records: [
            {
              subdomain: 'mail',
              fullDomain: 'mail.test.letsbe.cloud',
              expectedIp: '10.0.0.1',
              resolvedIp: '10.0.0.1',
              status: DnsRecordStatus.VERIFIED,
            },
          ],
        },
      } as any)

      const result = await getDnsStatus('order-123')

      expect(result.verification).not.toBeNull()
      expect(result.verification?.allPassed).toBe(true)
      expect(result.verification?.records).toHaveLength(1)
    })
  })
})
