const { describe, test, expect, beforeEach, afterEach } = require('bun:test')
const crypto = require('crypto')

const AFFILIATE_COMMISSION_RATE = 0.075
const AFFILIATE_COOKIE_TTL_MS = 30 * 24 * 60 * 60 * 1000

describe('Affiliate System', () => {
  describe('Affiliate Code Generation', () => {
    function generateTrackingCode() {
      return crypto.randomBytes(6).toString('hex').toUpperCase()
    }

    function sanitizeAffiliateCode(code) {
      return String(code || '')
        .trim()
        .toLowerCase()
        .replace(/[^a-z0-9\-]/g, '')
        .slice(0, 32)
    }

    function validateAffiliateCode(code) {
      if (!code || typeof code !== 'string') return { valid: false, error: 'Code is required' }
      const sanitized = sanitizeAffiliateCode(code)
      if (sanitized.length < 3) return { valid: false, error: 'Code must be at least 3 characters' }
      if (sanitized.length > 32) return { valid: false, error: 'Code must be 32 characters or less' }
      return { valid: true, sanitized }
    }

    test('generates tracking code', () => {
      const code = generateTrackingCode()
      expect(code.length).toBe(12)
      expect(/^[A-F0-9]+$/.test(code)).toBe(true)
    })

    test('generates unique codes', () => {
      const codes = new Set()
      for (let i = 0; i < 100; i++) {
        codes.add(generateTrackingCode())
      }
      expect(codes.size).toBe(100)
    })

    test('sanitizes affiliate codes', () => {
      expect(sanitizeAffiliateCode('TESTCODE')).toBe('testcode')
      expect(sanitizeAffiliateCode('Test-Code-123')).toBe('test-code-123')
      expect(sanitizeAffiliateCode('Test@Code!#$%')).toBe('testcode')
    })

    test('truncates long codes', () => {
      const longCode = 'a'.repeat(50)
      expect(sanitizeAffiliateCode(longCode).length).toBe(32)
    })

    test('validates affiliate code', () => {
      expect(validateAffiliateCode('valid-code').valid).toBe(true)
      expect(validateAffiliateCode('ab').valid).toBe(false)
      expect(validateAffiliateCode('').valid).toBe(false)
    })
  })

  describe('Affiliate Registration', () => {
    const affiliatesDb = []

    function createAffiliate(data) {
      const code = data.code || crypto.randomBytes(6).toString('hex').toUpperCase()
      return {
        id: crypto.randomUUID(),
        userId: data.userId,
        codes: [{
          code: code.toLowerCase(),
          label: data.label || 'Default link',
          createdAt: new Date().toISOString(),
        }],
        email: data.email,
        name: data.name,
        commissionRate: data.commissionRate || AFFILIATE_COMMISSION_RATE,
        earnings: [],
        totalEarnings: 0,
        paidOut: 0,
        pendingPayout: 0,
        status: 'active',
        createdAt: new Date().toISOString(),
      }
    }

    function findByCode(code) {
      const normalizedCode = code.toLowerCase()
      return affiliatesDb.find(a => 
        a.codes.some(c => c.code === normalizedCode)
      )
    }

    beforeEach(() => {
      affiliatesDb.length = 0
    })

    test('creates affiliate with default code', () => {
      const affiliate = createAffiliate({ userId: 'user-1', email: 'test@example.com' })

      expect(affiliate.codes.length).toBe(1)
      expect(affiliate.commissionRate).toBe(0.075)
      expect(affiliate.status).toBe('active')
    })

    test('creates affiliate with custom code', () => {
      const affiliate = createAffiliate({ userId: 'user-1', code: 'CUSTOM123' })

      expect(affiliate.codes[0].code).toBe('custom123')
    })

    test('finds affiliate by code', () => {
      affiliatesDb.push(createAffiliate({ userId: 'user-1', code: 'FINDME' }))

      const found = findByCode('findme')
      expect(found).toBeDefined()
      expect(found.codes[0].code).toBe('findme')
    })

    test('returns undefined for non-existent code', () => {
      const found = findByCode('notexist')
      expect(found).toBeUndefined()
    })
  })

  describe('Attribution Tracking', () => {
    const attributionStore = new Map()

    function recordAttribution(userId, affiliateCode, eventData) {
      const key = `attr_${userId}`
      attributionStore.set(key, {
        userId,
        affiliateCode,
        attributedAt: Date.now(),
        events: [eventData],
      })
    }

    function getAttribution(userId) {
      return attributionStore.get(`attr_${userId}`)
    }

    function isAttributionValid(attribution) {
      if (!attribution) return false
      const age = Date.now() - attribution.attributedAt
      return age < AFFILIATE_COOKIE_TTL_MS
    }

    beforeEach(() => {
      attributionStore.clear()
    })

    test('records attribution', () => {
      recordAttribution('user-1', 'affiliate123', { type: 'signup' })

      const attr = getAttribution('user-1')
      expect(attr).toBeDefined()
      expect(attr.affiliateCode).toBe('affiliate123')
    })

    test('checks attribution validity', () => {
      recordAttribution('user-1', 'test', { type: 'signup' })
      const attr = getAttribution('user-1')

      expect(isAttributionValid(attr)).toBe(true)
    })

    test('detects expired attribution', () => {
      attributionStore.set('attr_user-2', {
        userId: 'user-2',
        affiliateCode: 'old',
        attributedAt: Date.now() - AFFILIATE_COOKIE_TTL_MS - 1000,
        events: [],
      })

      const attr = getAttribution('user-2')
      expect(isAttributionValid(attr)).toBe(false)
    })

    test('affiliated cookie TTL is 30 days', () => {
      expect(AFFILIATE_COOKIE_TTL_MS).toBe(30 * 24 * 60 * 60 * 1000)
    })
  })

  describe('Commission Calculation', () => {
    function calculateCommission(amount, rate = AFFILIATE_COMMISSION_RATE) {
      return Math.round(amount * rate * 100) / 100
    }

    function calculateTieredCommission(amount, tier) {
      const rates = {
        bronze: 0.05,
        silver: 0.075,
        gold: 0.10,
        platinum: 0.15,
      }
      return calculateCommission(amount, rates[tier] || rates.bronze)
    }

    test('calculates standard commission', () => {
      expect(calculateCommission(100)).toBe(7.5)
      expect(calculateCommission(50)).toBe(3.75)
      expect(calculateCommission(1000)).toBe(75)
    })

    test('calculates tiered commissions', () => {
      expect(calculateTieredCommission(100, 'bronze')).toBe(5)
      expect(calculateTieredCommission(100, 'silver')).toBe(7.5)
      expect(calculateTieredCommission(100, 'gold')).toBe(10)
      expect(calculateTieredCommission(100, 'platinum')).toBe(15)
    })

    test('defaults to bronze for unknown tier', () => {
      expect(calculateTieredCommission(100, 'unknown')).toBe(5)
    })

    test('handles zero amount', () => {
      expect(calculateCommission(0)).toBe(0)
    })
  })

  describe('Earnings Tracking', () => {
    function addEarning(affiliate, amount, source) {
      const earning = {
        id: crypto.randomUUID(),
        amount,
        source,
        status: 'pending',
        createdAt: new Date().toISOString(),
      }

      affiliate.earnings.push(earning)
      affiliate.totalEarnings = (affiliate.totalEarnings || 0) + amount
      affiliate.pendingPayout = (affiliate.pendingPayout || 0) + amount

      return earning
    }

    function markAsPaid(affiliate, earningIds) {
      let paidAmount = 0
      for (const id of earningIds) {
        const earning = affiliate.earnings.find(e => e.id === id)
        if (earning && earning.status === 'pending') {
          earning.status = 'paid'
          earning.paidAt = new Date().toISOString()
          paidAmount += earning.amount
        }
      }
      affiliate.pendingPayout = Math.max(0, (affiliate.pendingPayout || 0) - paidAmount)
      affiliate.paidOut = (affiliate.paidOut || 0) + paidAmount
      return paidAmount
    }

    test('adds earning to affiliate', () => {
      const affiliate = { earnings: [], totalEarnings: 0, pendingPayout: 0, paidOut: 0 }
      addEarning(affiliate, 10, 'subscription')

      expect(affiliate.earnings.length).toBe(1)
      expect(affiliate.totalEarnings).toBe(10)
      expect(affiliate.pendingPayout).toBe(10)
    })

    test('marks earnings as paid', () => {
      const affiliate = { earnings: [], totalEarnings: 0, pendingPayout: 0, paidOut: 0 }
      const e1 = addEarning(affiliate, 10, 'sub1')
      const e2 = addEarning(affiliate, 20, 'sub2')

      const paid = markAsPaid(affiliate, [e1.id])

      expect(paid).toBe(10)
      expect(affiliate.paidOut).toBe(10)
      expect(affiliate.pendingPayout).toBe(20)
    })

    test('does not double pay earnings', () => {
      const affiliate = { earnings: [], totalEarnings: 0, pendingPayout: 0, paidOut: 0 }
      const earning = addEarning(affiliate, 10, 'sub')

      markAsPaid(affiliate, [earning.id])
      const paidAgain = markAsPaid(affiliate, [earning.id])

      expect(paidAgain).toBe(0)
      expect(affiliate.paidOut).toBe(10)
    })
  })

  describe('Withdrawal Processing', () => {
    const withdrawalsDb = []
    const MIN_WITHDRAWAL_AMOUNT = 10

    function createWithdrawal(affiliateId, amount, paymentMethod) {
      if (amount < MIN_WITHDRAWAL_AMOUNT) {
        return { error: `Minimum withdrawal is $${MIN_WITHDRAWAL_AMOUNT}` }
      }

      return {
        id: crypto.randomUUID(),
        affiliateId,
        amount,
        paymentMethod,
        status: 'pending',
        createdAt: new Date().toISOString(),
        processedAt: null,
      }
    }

    function processWithdrawal(withdrawal, status) {
      withdrawal.status = status
      withdrawal.processedAt = new Date().toISOString()
      return withdrawal
    }

    beforeEach(() => {
      withdrawalsDb.length = 0
    })

    test('creates withdrawal request', () => {
      const withdrawal = createWithdrawal('aff-1', 50, 'paypal')

      expect(withdrawal.id).toBeDefined()
      expect(withdrawal.status).toBe('pending')
      expect(withdrawal.amount).toBe(50)
    })

    test('rejects withdrawal below minimum', () => {
      const result = createWithdrawal('aff-1', 5, 'paypal')

      expect(result.error).toBeDefined()
    })

    test('processes withdrawal', () => {
      const withdrawal = createWithdrawal('aff-1', 50, 'paypal')
      processWithdrawal(withdrawal, 'completed')

      expect(withdrawal.status).toBe('completed')
      expect(withdrawal.processedAt).toBeDefined()
    })
  })

  describe('Affiliate Links', () => {
    function generateAffiliateLink(baseUrl, code, params = {}) {
      const url = new URL(baseUrl)
      url.searchParams.set('ref', code)
      for (const [key, value] of Object.entries(params)) {
        url.searchParams.set(key, value)
      }
      return url.toString()
    }

    function parseAffiliateLink(urlString) {
      try {
        const url = new URL(urlString)
        const ref = url.searchParams.get('ref')
        return ref ? { code: ref.toLowerCase() } : null
      } catch {
        return null
      }
    }

    test('generates affiliate link', () => {
      const link = generateAffiliateLink('https://example.com/pricing', 'MYCODE')

      expect(link).toContain('ref=MYCODE')
      expect(link).toContain('example.com/pricing')
    })

    test('includes additional parameters', () => {
      const link = generateAffiliateLink('https://example.com', 'CODE', { utm_source: 'email' })

      expect(link).toContain('utm_source=email')
    })

    test('parses affiliate link', () => {
      const parsed = parseAffiliateLink('https://example.com?ref=TESTCODE')

      expect(parsed.code).toBe('testcode')
    })

    test('returns null for link without ref', () => {
      const parsed = parseAffiliateLink('https://example.com?page=home')

      expect(parsed).toBe(null)
    })
  })

  describe('Affiliate Dashboard Stats', () => {
    function calculateStats(affiliate) {
      const totalClicks = affiliate.clicks || 0
      const totalSignups = affiliate.signups || 0
      const totalConversions = affiliate.conversions || 0
      const conversionRate = totalClicks > 0 ? (totalConversions / totalClicks) * 100 : 0

      return {
        clicks: totalClicks,
        signups: totalSignups,
        conversions: totalConversions,
        conversionRate: Math.round(conversionRate * 100) / 100,
        totalEarnings: affiliate.totalEarnings || 0,
        pendingPayout: affiliate.pendingPayout || 0,
        paidOut: affiliate.paidOut || 0,
      }
    }

    test('calculates affiliate stats', () => {
      const affiliate = {
        clicks: 1000,
        signups: 50,
        conversions: 25,
        totalEarnings: 100,
        pendingPayout: 50,
        paidOut: 50,
      }

      const stats = calculateStats(affiliate)

      expect(stats.clicks).toBe(1000)
      expect(stats.conversions).toBe(25)
      expect(stats.conversionRate).toBe(2.5)
      expect(stats.totalEarnings).toBe(100)
    })

    test('handles zero clicks', () => {
      const stats = calculateStats({ clicks: 0, conversions: 0 })

      expect(stats.conversionRate).toBe(0)
    })
  })
})
