import { ethers } from 'ethers'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
  calculateActualCapacity,
  calculateStorageAllowances,
  calculateStorageFromUSDFC,
  checkFILBalance,
  checkUSDFCBalance,
  depositUSDFC,
  getPaymentStatus,
  setServiceApprovals,
} from '../../core/payments/index.js'
import { formatFIL, formatUSDFC } from '../../core/utils/format.js'
import { assertPriceNonZero } from '../../core/utils/validate-pricing.js'
import { parseStorageAllowance } from '../../payments/setup.js'

// Mock Synapse SDK
vi.mock('@filoz/synapse-sdk', () => {
  const mockSynapse = {
    getProvider: vi.fn(),
    getSigner: vi.fn(),
    getClient: vi.fn(),
    getNetwork: vi.fn(),
    getPaymentsAddress: vi.fn(),
    getWarmStorageAddress: vi.fn(),
    payments: {
      walletBalance: vi.fn(),
      balance: vi.fn(),
      serviceApproval: vi.fn(),
      allowance: vi.fn(),
      approve: vi.fn(),
      deposit: vi.fn(),
      approveService: vi.fn(),
    },
    storage: {
      getStorageInfo: vi.fn(),
    },
  }

  return {
    Synapse: {
      create: vi.fn().mockResolvedValue(mockSynapse),
    },
    TOKENS: {
      USDFC: 'USDFC',
    },
    TIME_CONSTANTS: {
      EPOCHS_PER_DAY: 2880n,
      EPOCHS_PER_MONTH: 86400n,
    },
    SIZE_CONSTANTS: {
      MIN_UPLOAD_SIZE: 127,
    },
    METADATA_KEYS: {
      WITH_IPFS_INDEXING: 'withIPFSIndexing',
      IPFS_ROOT_CID: 'ipfsRootCid',
    },
  }
})

describe('assertPriceNonZero', () => {
  it('throws when pricePerTiBPerEpoch is zero', () => {
    expect(() => assertPriceNonZero(0n)).toThrow('Invalid pricePerTiBPerEpoch: must be positive non-zero value')
  })

  it('throws when pricePerTiBPerEpoch is negative', () => {
    expect(() => assertPriceNonZero(-10n)).toThrow('Invalid pricePerTiBPerEpoch: must be positive non-zero value')
  })

  it('does not throw for positive price', () => {
    expect(() => assertPriceNonZero(1n)).not.toThrow()
  })
})

describe('Payment Setup Tests', () => {
  let mockSynapse: any
  let mockProvider: any
  let mockSigner: any

  beforeEach(() => {
    // Reset mocks
    vi.clearAllMocks()

    // Create mock instances
    mockProvider = {
      getBalance: vi.fn().mockResolvedValue(ethers.parseEther('5')),
    }

    mockSigner = {
      getAddress: vi.fn().mockResolvedValue('0x1234567890123456789012345678901234567890'),
    }

    // Create mock Synapse instance
    mockSynapse = {
      getProvider: vi.fn().mockReturnValue(mockProvider),
      getSigner: vi.fn().mockReturnValue(mockSigner),
      getClient: vi.fn().mockReturnValue(mockSigner),
      getNetwork: vi.fn().mockReturnValue('calibration'),
      getPaymentsAddress: vi.fn().mockReturnValue('0xpayments'),
      getWarmStorageAddress: vi.fn().mockReturnValue('0xwarmstorage'),
      payments: {
        walletBalance: vi.fn().mockResolvedValue(ethers.parseUnits('100', 18)),
        balance: vi.fn().mockResolvedValue(ethers.parseUnits('10', 18)),
        serviceApproval: vi.fn().mockResolvedValue({
          rateAllowance: ethers.parseUnits('0.0001', 18),
          lockupAllowance: ethers.parseUnits('2', 18),
          rateUsed: 0n,
          lockupUsed: 0n,
        }),
        allowance: vi.fn().mockResolvedValue(ethers.parseUnits('0', 18)),
        depositWithPermitAndApproveOperator: vi.fn().mockResolvedValue({
          wait: vi.fn(),
          hash: '0xdepositWithPermitAndApproveOperator',
        }),
        deposit: vi.fn().mockResolvedValue({
          wait: vi.fn(),
          hash: '0xdeposit',
        }),
        approveService: vi.fn().mockResolvedValue({
          wait: vi.fn(),
          hash: '0xservice',
        }),
      },
      storage: {
        getStorageInfo: vi.fn().mockResolvedValue({
          pricing: {
            noCDN: {
              perTiBPerEpoch: ethers.parseUnits('0.00002893519', 18), // 2.5 USDFC/TiB/month
              perTiBPerDay: ethers.parseUnits('0.08333333', 18),
              perTiBPerMonth: ethers.parseUnits('2.5', 18),
            },
          },
        }),
      },
    }
  })

  describe('checkFILBalance', () => {
    it('should check FIL balance and network correctly', async () => {
      const result = await checkFILBalance(mockSynapse)

      expect(result.balance).toBe(ethers.parseEther('5'))
      expect(result.isCalibnet).toBe(true)
      expect(result.hasSufficientGas).toBe(true)
    })

    it('should detect insufficient gas', async () => {
      mockProvider.getBalance.mockResolvedValue(ethers.parseEther('0.05'))

      const result = await checkFILBalance(mockSynapse)

      expect(result.hasSufficientGas).toBe(false)
    })
  })

  describe('checkUSDFCBalance', () => {
    it('should return USDFC wallet balance', async () => {
      const balance = await checkUSDFCBalance(mockSynapse)

      expect(balance).toBe(ethers.parseUnits('100', 18))
      expect(mockSynapse.payments.walletBalance).toHaveBeenCalledWith('USDFC')
    })
  })

  describe('getPaymentStatus', () => {
    it('should return complete payment status', async () => {
      const status = await getPaymentStatus(mockSynapse)

      expect(status.network).toBe('calibration')
      expect(status.address).toBe('0x1234567890123456789012345678901234567890')
      expect(status.filBalance).toBe(ethers.parseEther('5'))
      expect(status.walletUsdfcBalance).toBe(ethers.parseUnits('100', 18))
      expect(status.filecoinPayBalance).toBe(ethers.parseUnits('10', 18))
      expect(status.currentAllowances.rateAllowance).toBe(ethers.parseUnits('0.0001', 18))
    })
  })

  describe('depositUSDFC', () => {
    it('should deposit USDFC without approval when allowance sufficient', async () => {
      mockSynapse.payments.allowance.mockResolvedValue(ethers.parseUnits('10', 18))

      const result = await depositUSDFC(mockSynapse, ethers.parseUnits('5', 18))

      expect(result.depositTx).toBe('0xdepositWithPermitAndApproveOperator')
      expect(mockSynapse.payments.depositWithPermitAndApproveOperator).toHaveBeenCalled()
    })

    it('should approve and deposit when allowance insufficient', async () => {
      mockSynapse.payments.allowance.mockResolvedValue(ethers.parseUnits('0', 18))

      const result = await depositUSDFC(mockSynapse, ethers.parseUnits('5', 18))

      expect(result.depositTx).toBe('0xdepositWithPermitAndApproveOperator')
      expect(mockSynapse.payments.depositWithPermitAndApproveOperator).toHaveBeenCalled()
    })
  })

  describe('setServiceApprovals', () => {
    it('should set service approvals with correct parameters', async () => {
      const rateAllowance = ethers.parseUnits('0.0001', 18)
      const lockupAllowance = ethers.parseUnits('2', 18)

      const txHash = await setServiceApprovals(mockSynapse, rateAllowance, lockupAllowance)

      expect(txHash).toBe('0xservice')
      expect(mockSynapse.payments.approveService).toHaveBeenCalledWith(
        '0xwarmstorage',
        rateAllowance,
        lockupAllowance,
        86400n, // 30 days * 2880 epochs/day (bigint)
        'USDFC'
      )
    })
  })

  describe('calculateStorageAllowances', () => {
    it('should calculate allowances for 1 TiB/month', () => {
      const pricePerTiBPerEpoch = ethers.parseUnits('0.0000565', 18)
      const allowances = calculateStorageAllowances(1, pricePerTiBPerEpoch)

      expect(allowances.storageCapacityTiB).toBe(1)
      expect(allowances.rateAllowance).toBe(ethers.parseUnits('0.0000565', 18))
      expect(allowances.lockupAllowance).toBe(
        ethers.parseUnits('0.0000565', 18) * 2880n * 30n // rate * epochs/day * 30 days
      )
    })

    it('should calculate allowances for fractional TiB', () => {
      const pricePerTiBPerEpoch = ethers.parseUnits('0.0000565', 18)
      const allowances = calculateStorageAllowances(0.5, pricePerTiBPerEpoch)

      expect(allowances.storageCapacityTiB).toBe(0.5)
      // 0.5 TiB
      expect(allowances.rateAllowance).toBe(ethers.parseUnits('0.00002825', 18))
    })

    it('should calculate allowances for 1.5 TiB correctly', async () => {
      const pricePerTiBPerEpoch = ethers.parseUnits('0.0000565', 18)
      const allowances = calculateStorageAllowances(1.5, pricePerTiBPerEpoch)

      expect(allowances.storageCapacityTiB).toBe(1.5)
      // 1.5 TiB
      expect(allowances.rateAllowance).toBe(ethers.parseUnits('0.00008475', 18))
      expect(allowances.lockupAllowance).toBe(
        ethers.parseUnits('0.00008475', 18) * 2880n * 30n // rate * epochs/day * 30 days
      )
    })

    it('should calculate allowances for 1 GiB/month (small storage amount)', () => {
      const pricePerTiBPerEpoch = ethers.parseUnits('0.0000565', 18)
      const storageTiB = 1 / 1024 // 1 GiB = 1/1024 TiB ~= 0.0009765625 TiB
      const allowances = calculateStorageAllowances(storageTiB, pricePerTiBPerEpoch)

      expect(allowances.storageCapacityTiB).toBe(storageTiB)
      expect(allowances.rateAllowance).toBeGreaterThan(0n)
      expect(allowances.lockupAllowance).toBeGreaterThan(0n)

      const roundTripTiB = calculateActualCapacity(allowances.rateAllowance, pricePerTiBPerEpoch)
      expect(roundTripTiB).toBeCloseTo(storageTiB, 6)
    })

    it('should calculate allowances for 512 MiB/month', () => {
      const pricePerTiBPerEpoch = ethers.parseUnits('0.0000565', 18)
      const storageTiB = 512 / (1024 * 1024) // 512 MiB = 512/(1024*1024) TiB ~= 0.00048828125 TiB
      const allowances = calculateStorageAllowances(storageTiB, pricePerTiBPerEpoch)

      expect(allowances.storageCapacityTiB).toBe(storageTiB)
      expect(allowances.rateAllowance).toBeGreaterThan(0n)
      expect(allowances.lockupAllowance).toBeGreaterThan(0n)

      const roundTripTiB = calculateActualCapacity(allowances.rateAllowance, pricePerTiBPerEpoch)
      expect(roundTripTiB).toBeCloseTo(storageTiB, 6)
    })

    it('should calculate allowances for 1 MiB/month', () => {
      const pricePerTiBPerEpoch = ethers.parseUnits('0.0000565', 18)
      const storageTiB = 1 / (1024 * 1024) // 1 MiB in TiB
      const allowances = calculateStorageAllowances(storageTiB, pricePerTiBPerEpoch)

      expect(allowances.storageCapacityTiB).toBe(storageTiB)
      expect(allowances.rateAllowance).toBeGreaterThan(0n)
      expect(allowances.lockupAllowance).toBeGreaterThan(0n)

      const roundTripTiB = calculateActualCapacity(allowances.rateAllowance, pricePerTiBPerEpoch)
      expect(roundTripTiB).toBeCloseTo(storageTiB, 6)
    })

    it('should handle very large TiB values without overflow', () => {
      const pricePerTiBPerEpoch = ethers.parseUnits('0.0000565', 18)
      // 900 billion TiB (if we multiplied this by STORAGE_SCALE_MAX, it would overflow)
      const storageTiB = 900_000_000_000

      const allowances = calculateStorageAllowances(storageTiB, pricePerTiBPerEpoch)

      // rateAllowance should be price * storageTiB exactly representable via bigint math
      const expectedRate = (pricePerTiBPerEpoch * BigInt(storageTiB)) / 1n
      expect(allowances.rateAllowance).toBe(expectedRate)
    })
  })

  describe('parseStorageAllowance', () => {
    it('should parse TiB/month format', () => {
      const tibPerMonth = parseStorageAllowance('2TiB/month')

      expect(tibPerMonth).toBe(2)
    })

    it('should parse GiB/month format', () => {
      const tibPerMonth = parseStorageAllowance('512GiB/month')

      expect(tibPerMonth).toBe(0.5)
    })

    it('should parse MiB/month format', () => {
      const tibPerMonth = parseStorageAllowance(`524288MiB/month`) // 512 GiB

      expect(tibPerMonth).toBe(0.5)
    })

    it('should return null for direct USDFC/epoch format', () => {
      const tibPerMonth = parseStorageAllowance('0.0001')

      expect(tibPerMonth).toBeNull()
    })

    it('should throw on invalid format', () => {
      expect(() => parseStorageAllowance('invalid')).toThrow()
    })
  })

  describe('formatUSDFC', () => {
    it('should format USDFC amounts correctly', () => {
      expect(formatUSDFC(ethers.parseUnits('1.2345', 18))).toBe('1.2345')
      expect(formatUSDFC(ethers.parseUnits('1.23456789', 18))).toBe('1.2346')
      expect(formatUSDFC(ethers.parseUnits('1000', 18))).toBe('1000.0000')
      expect(formatUSDFC(ethers.parseUnits('0.0001', 18), 6)).toBe('0.000100')
    })
  })

  describe('formatFIL', () => {
    it('should format FIL amounts with correct unit', () => {
      expect(formatFIL(ethers.parseEther('1.5'), false)).toBe('1.5000 FIL')
      expect(formatFIL(ethers.parseEther('1.5'), true)).toBe('1.5000 tFIL')
      expect(formatFIL(ethers.parseEther('0.0001'), false)).toBe('0.0001 FIL')
    })
  })

  describe('calculateActualCapacity', () => {
    it('should calculate capacity from rate allowance with high precision', () => {
      const pricePerTiBPerEpoch = ethers.parseUnits('0.0000565', 18)
      const storageTiB = 1 / 1024 // 1 GiB/month
      const rateAllowance = calculateStorageAllowances(storageTiB, pricePerTiBPerEpoch).rateAllowance

      const capacityTiB = calculateActualCapacity(rateAllowance, pricePerTiBPerEpoch)

      const expectedTiB = 1 / 1024 // ~= 0.0009765625
      expect(capacityTiB).toBeCloseTo(expectedTiB, 5)
    })

    it('throws when pricePerTiBPerEpoch is zero', () => {
      expect(() => calculateActualCapacity(ethers.parseUnits('1', 18), 0n)).toThrow(
        'Invalid pricePerTiBPerEpoch: must be positive non-zero value'
      )
    })
  })

  describe('calculateStorageFromUSDFC', () => {
    it('should calculate storage capacity from USDFC amount with high precision', () => {
      const pricePerTiBPerEpoch = ethers.parseUnits('0.0000565', 18)
      // 30 days worth of 1GiB/month = 0.0047644416 USDFC
      const usdfcAmount = ethers.parseUnits('0.0047644416', 18)

      const capacityTiB = calculateStorageFromUSDFC(usdfcAmount, pricePerTiBPerEpoch)

      const expectedTiB = 1 / 1024 // ~= 0.0009765625
      expect(capacityTiB).toBeCloseTo(expectedTiB, 5)
    })

    it('throws when pricePerTiBPerEpoch is zero', () => {
      expect(() => calculateStorageFromUSDFC(ethers.parseUnits('1', 18), 0n)).toThrow(
        'Invalid pricePerTiBPerEpoch: must be positive non-zero value'
      )
    })

    it('returns 0 if usdfcAmount is 0', () => {
      const usdfcAmount = ethers.parseUnits('0', 18)
      const pricePerTiBPerEpoch = ethers.parseUnits('0.0005', 18)
      const capacityTiB = calculateStorageFromUSDFC(usdfcAmount, pricePerTiBPerEpoch)

      expect(capacityTiB).toBe(0)
    })

    // Verify pricePerTibPerEpoch needed to get 1TiB/month with 1USDFC given 30-day lockup
    // With 30-day lockup: 1 USDFC / (30 days * 2880 epochs/day) = 1 USDFC / 86400 epochs
    // For 1 TiB capacity: pricePerTiBPerEpoch = 1 / 86400 = 0.000011574074 USDFC
    it('should return capacity of 1 when pricePerTibPerEpoch is low', () => {
      const usdfcAmount = ethers.parseUnits('1', 18)
      const pricePerTiBPerEpoch = ethers.parseUnits('0.000011574074', 18)
      const capacityTiB = calculateStorageFromUSDFC(usdfcAmount, pricePerTiBPerEpoch)
      // within 10 decimal places accuracy of 1
      expect(capacityTiB).toBeCloseTo(1, 10)
    })

    it('should return lower capacity as pricePerTibPerEpoch increases', () => {
      const usdfcAmount = ethers.parseUnits('1', 18)
      const pricePerTiBPerEpoch = ethers.parseUnits('0.00005', 18)
      const capacityTiB = calculateStorageFromUSDFC(usdfcAmount, pricePerTiBPerEpoch)
      expect(
        calculateStorageFromUSDFC(usdfcAmount, pricePerTiBPerEpoch + ethers.parseUnits('0.00001', 18))
      ).toBeLessThan(capacityTiB)
    })

    it('should return higher capacity as pricePerTibPerEpoch decreases', () => {
      const usdfcAmount = ethers.parseUnits('1', 18)
      const pricePerTiBPerEpoch = ethers.parseUnits('0.00005', 18)
      const capacityTiB = calculateStorageFromUSDFC(usdfcAmount, pricePerTiBPerEpoch)
      expect(
        calculateStorageFromUSDFC(usdfcAmount, pricePerTiBPerEpoch - ethers.parseUnits('0.00001', 18))
      ).toBeGreaterThan(capacityTiB)
    })
  })
})
