# frozen_string_literal: true

require "bigdecimal"
require "em-http"
require "test_helper"
require "customer"
require "low_balance"
require "sim"

load File.expand_path("../bin/sim_job", __dir__)

class SimJobTest < Minitest::Test
	class Ledger
		attr_reader :charges

		def initialize(balances)
			@balances = balances
			@charges = []
		end

		def balance_for(customer_id)
			@balances.fetch(customer_id, BigDecimal("0"))
		end

		def charge(customer_id, amount)
			@balances[customer_id] = balance_for(customer_id) - amount
			@charges << [customer_id, amount]
		end
	end

	class CustomerRepo
		def initialize(customers, ledger)
			@customers = customers
			@ledger = ledger
		end

		def find(customer_id)
			customer = @customers.fetch(customer_id)
			EMPromise.resolve(customer.with(balance: balance_for(customer)))
		end

		def balance_for(customer)
			@ledger.balance_for(customer.billing_customer_id)
		end
	end

	class RecordingAction
		attr_accessor :customer
		attr_reader :balances_seen, :iccid

		def initialize(iccid, customer, ledger:, price: BigDecimal("5"), error: nil)
			@iccid = iccid
			@customer = customer
			@ledger = ledger
			@price = price
			@error = error
			@balances_seen = []
		end

		def call
			@balances_seen << customer.balance
			return EMPromise.reject(@error) if @error

			if customer.balance >= @price
				@ledger.charge(customer.billing_customer_id, @price)
			end
			EMPromise.resolve(nil)
		end
	end

	class SuccessfulLowBalance
		def initialize(amount)
			@amount = amount
		end

		def notify!
			EMPromise.resolve(@amount)
		end
	end

	def test_process_sim_actions_reloads_balance_between_actions
		ledger = Ledger.new("parent" => BigDecimal("5"))
		repo = CustomerRepo.new(
			{
				"parent" => customer("parent"),
				"first" => customer("first", parent_customer_id: "parent"),
				"second" => customer("second", parent_customer_id: "parent")
			},
			ledger
		)
		first = RecordingAction.new(
			"first", customer("first", parent_customer_id: "parent"), ledger: ledger
		)
		second = RecordingAction.new(
			"second", customer("second", parent_customer_id: "parent"), ledger: ledger
		)

		process_sim_actions([first, second], customer_repo: repo).sync

		assert_equal [BigDecimal("5")], first.balances_seen
		assert_equal [BigDecimal("0")], second.balances_seen
		assert_equal [["parent", BigDecimal("5")]], ledger.charges
	end
	em :test_process_sim_actions_reloads_balance_between_actions

	def test_process_sim_actions_continues_after_failure
		ledger = Ledger.new("first" => BigDecimal("5"), "second" => BigDecimal("5"))
		repo = CustomerRepo.new(
			{
				"first" => customer("first"),
				"second" => customer("second")
			},
			ledger
		)
		error = RuntimeError.new("boom")
		first = RecordingAction.new(
			"first", customer("first"), ledger: ledger, error: error
		)
		second = RecordingAction.new("second", customer("second"), ledger: ledger)

		process_sim_actions([first, second], customer_repo: repo).sync

		assert_equal [BigDecimal("5")], first.balances_seen
		assert_equal [BigDecimal("5")], second.balances_seen
		assert_equal [["second", BigDecimal("5")]], ledger.charges
	end
	em :test_process_sim_actions_continues_after_failure

	def test_job_customer_preserves_billing_customer_when_balance_changes
		parent = customer(
			"parent",
			balance: BigDecimal("45"),
			auto_top_up_amount: 15
		)
		child = customer(
			"child",
			parent_customer_id: "parent",
			balance: BigDecimal("0")
		)

		updated = JobCustomer.new(child, billing_customer: parent)
			.with(balance: BigDecimal("40"))
		billing_customer = updated.billing_customer.sync

		assert_kind_of JobCustomer, updated
		assert_equal "child", updated.customer_id
		assert_equal BigDecimal("40"), updated.balance
		assert_kind_of JobCustomer, billing_customer
		assert_equal "parent", billing_customer.customer_id
		assert_equal BigDecimal("40"), billing_customer.balance
	end
	em :test_job_customer_preserves_billing_customer_when_balance_changes

	def test_sim_top_up_refills_when_refill_exactly_fits_monthly_limit
		top_up = SimTopUp.new(sim, customer: customer(balance: BigDecimal("100")))
		refill = nil

		top_up.stub(:amount_spent, EMPromise.resolve(BigDecimal("1"))) do
			top_up.stub(:monthly_limit, EMPromise.resolve(BigDecimal("46"))) do
				top_up.stub(:refill_price, BigDecimal("45")) do
					top_up.stub(:refill_and_bill, lambda { |*args|
						refill = args
						EMPromise.resolve(nil)
					}) do
						top_up.call.sync
					end
				end
			end
		end

		assert_equal(
			[5120, BigDecimal("45"), "5GB Data Topup for 123"],
			refill
		)
	end
	em :test_sim_top_up_refills_when_refill_exactly_fits_monthly_limit

	def test_sim_top_up_warns_when_refill_would_exceed_monthly_limit
		top_up = SimTopUp.new(sim, customer: customer(balance: BigDecimal("100")))
		warned = false
		refilled = false

		top_up.stub(:amount_spent, EMPromise.resolve(BigDecimal("45"))) do
			top_up.stub(:monthly_limit, EMPromise.resolve(BigDecimal("46"))) do
				top_up.stub(:refill_price, BigDecimal("45")) do
					top_up.stub(:refill_and_bill, lambda { |*|
						refilled = true
						EMPromise.resolve(nil)
					}) do
						top_up.stub(:warn, lambda {
							warned = true
							EMPromise.resolve(nil)
						}) do
							top_up.call.sync
						end
					end
				end
			end
		end

		assert warned
		refute refilled
	end
	em :test_sim_top_up_warns_when_refill_would_exceed_monthly_limit

	def test_sim_annual_retries_with_low_balance_result_added_to_balance
		annual = SimAnnual.new(sim, customer: customer(balance: BigDecimal("0")))
		balances_billed = []
		low_balance = SuccessfulLowBalance.new(BigDecimal("10"))

		LowBalance.stub(:for, EMPromise.resolve(low_balance)) do
			annual.stub(:annual_price, BigDecimal("10")) do
				annual.stub(:refill_and_bill, lambda { |*|
					balances_billed << annual.customer.balance
					EMPromise.resolve(nil)
				}) do
					annual.call.sync
				end
			end
		end

		assert_equal [BigDecimal("10")], balances_billed
	end
	em :test_sim_annual_retries_with_low_balance_result_added_to_balance

	def sim
		SIM.new(
			iccid: "123",
			remaining_usage_kb: 1,
			remaining_days: 365,
			notes: ""
		)
	end
end
