# frozen_string_literal: true

begin
	require "simplecov"
	SimpleCov.start do
		add_filter "/test/"
		enable_coverage :branch
	end
rescue LoadError
	nil
end

require "em_promise"
require "fiber"
require "minitest/autorun"
require "rantly/minitest_extensions"
require "sentry-ruby"
require "webmock/minitest"
begin
	require "pry-rescue/minitest"
	require "pry-reload"

	module Minitest
		class Test
			alias old_capture_exceptions capture_exceptions
			def capture_exceptions
				old_capture_exceptions do
					yield
				rescue Minitest::Skip => e
					failures << e
				end
			end
		end
	end
rescue LoadError
	# Just helpers for dev, no big deal if missing
	nil
end

require "backend_sgx"
require "tel_selections"

$VERBOSE = nil
Sentry.init

def mksgx(customer_id="bogus", **kwargs)
	kwargs.delete(:sgx) || BackendSgx.new(
		jid: Blather::JID.new(CONFIG[:sgx]),
		creds: CONFIG[:creds],
		from_jid: ProxiedJID.proxy(
			"customer_#{customer_id}",
			CONFIG[:component][:jid]
		).__getobj__,
		ogm_url: NotLoaded.new("ogm_url"),
		fwd: NotLoaded.new("fwd"),
		transcription_enabled: NotLoaded.new("transcription_enabled"),
		registered?: NotLoaded.new("registered?"),
		**kwargs
	)
end

def customer(
	customer_id="test",
	plan_name: nil,
	jid: Blather::JID.new("#{customer_id}@example.net"),
	expires_at: Time.now,
	auto_top_up_amount: 0,
	**kwargs
)
	Customer.extract(
		customer_id,
		jid,
		sgx: kwargs.delete(:sgx) || mksgx(customer_id),
		plan_name: plan_name,
		expires_at: expires_at,
		auto_top_up_amount: auto_top_up_amount,
		**kwargs
	)
end

CONFIG = {
	sgx: "sgx",
	component: {
		jid: "component"
	},
	creds: {
		account: "test_bw_account",
		username: "test_bw_user",
		password: "test_bw_password"
	},
	notify_from: "notify_from@example.org",
	activation_amount: 1,
	activation_amount_accept: 1,
	plans: [
		{
			name: "test_usd",
			currency: :USD,
			monthly_price: 10000,
			messages: :unlimited,
			minutes: { included: 10440, price: 87 },
			allow_register: true
		},
		{
			name: "test_bad_currency",
			currency: :BAD
		},
		{
			name: "test_usd_no_register",
			currency: :USD,
			monthly_price: 10000,
			messages: :unlimited,
			minutes: { included: 10440, price: 87 },
			allow_register: false
		},
		{
			name: "test_cad",
			currency: :CAD,
			monthly_price: 10000
		}
	],
	braintree: {
		merchant_accounts: {
			USD: "merchant_usd"
		}
	},
	sip: {
		realm: "sip.example.com",
		app: "sipappid"
	},
	xep0157: [
		{ var: "support-addresses", value: "xmpp:tel@cheogram.com" }
	],
	credit_card_url: ->(*) { "http://creditcard.example.com?" },
	electrum_notify_url: ->(*) { "http://notify.example.com" },
	keep_area_codes: ["556"],
	keep_area_codes_in: {
		account: "moveto",
		site_id: "movetosite",
		sip_peer_id: "movetopeer"
	},
	upstream_domain: "example.net",
	approved_domains: {
		"approved.example.com": nil,
		"refer.example.com": "refer_to"
	},
	parented_domains: {
		"parented.example.com" => {
			customer_id: "1234",
			plan_name: "test_usd"
		}
	},
	bandwidth_site: "test_site",
	bandwidth_peer: "test_peer",
	keepgo: { api_key: "keepgokey", access_token: "keepgotoken" },
	snikket_hosting_api: "snikket.example.com",
	onboarding_domain: "onboarding.example.com",
	adr: "A Mailing Address",
	interac: "interac@example.com",
	support_link: ->(*) { "https://support.com" }
}.freeze

def panic(e)
	raise e
end

LOG = Class.new {
	def child(*)
		Minitest::Mock.new
	end

	def debug(*); end

	def info(*); end

	def error(*); end
}.new.freeze

def log
	LOG
end

BLATHER = Class.new {
	def <<(*); end
}.new.freeze

def execute_command(
	iq=Blather::Stanza::Iq::Command.new.tap { |i| i.from = "test@example.com" },
	blather: BLATHER,
	&blk
)
	Command::Execution.new(
		Minitest::Mock.new,
		blather,
		:to_s.to_proc,
		iq
	).execute(&blk).sync
end

class NotLoaded < BasicObject
	def inspect
		"<NotLoaded #{@name}>"
	end
end

class Matching
	def initialize(&block)
		@block = block
	end

	def ===(other)
		@block.call(other)
	end
end

class PromiseMock < Minitest::Mock
	def then(succ=nil, _=nil)
		if succ
			succ.call(self)
		else
			yield self
		end
	end

	def is_a?(_klass)
		false
	end
end

class FakeTelSelections
	def initialize
		@selections = {}
	end

	def set(jid, tel)
		@selections[jid] = EMPromise.resolve(
			TelSelections::HaveTel.new(tel.pending_value)
		)
	end

	def delete(jid)
		@selections.delete(jid)
		EMPromise.resolve("OK")
	end

	def [](jid)
		@selections.fetch(jid) do
			TelSelections::ChooseTel.new(db: FakeDB.new, memcache: FakeMemcache.new)
		end
	end
end

class FakeRedis
	def initialize(values={})
		@values = values
	end

	def set(key, value)
		@values[key] = value
		EMPromise.resolve("OK")
	end

	def setex(key, _expiry, value)
		set(key, value)
	end

	def del(key)
		@values.delete(key)
		EMPromise.resolve("OK")
	end

	def mget(*keys)
		EMPromise.all(keys.map(&method(:get)))
	end

	def get(key)
		EMPromise.resolve(@values[key])
	end

	def getbit(key, bit)
		get(key).then { |v| v.to_i.to_s(2)[bit].to_i }
	end

	def bitfield(key, *ops)
		get(key).then do |v|
			bits = v.to_i.to_s(2)
			ops.each_slice(3).map do |(op, encoding, offset)|
				raise "unsupported bitfield op" unless op == "GET"
				raise "unsupported bitfield op" unless encoding == "u1"

				bits[offset].to_i
			end
		end
	end

	def hget(key, field)
		@values.dig(key, field)
	end

	def hexists(key, field)
		hget(key, field).nil? ? 0 : 1
	end

	def hincrby(key, field, incrby)
		@values[key] ||= {}
		@values[key][field] ||= 0
		@values[key][field] += incrby
	end

	def sadd(key, member)
		@values[key] ||= Set.new
		@values[key] << member
	end

	def srem(key, member)
		@values[key]&.delete(member)
	end

	def scard(key)
		@values[key]&.size || 0
	end

	def smembers(key)
		@values[key]&.to_a || []
	end

	def sismember(key, value)
		smembers(key).include?(value)
	end

	def expire(_, _); end

	def exists(*keys)
		EMPromise.resolve(
			@values.select { |k, _| keys.include? k }.size
		)
	end

	def lindex(key, index)
		get(key).then { |v| v&.fetch(index) }
	end

	def incr(key)
		get(key).then { |v|
			n = v ? v + 1 : 0
			set(key, n).then { n }
		}
	end
end

class FakeDB
	class MultiResult
		def initialize(*args)
			@results = args
		end

		def to_a
			@results.shift
		end
	end

	def initialize(items={})
		@items = items
	end

	def query_defer(_, args)
		EMPromise.resolve(@items.fetch(args, []).to_a)
	end

	def query_one(_, *args, field_names_as: :symbol, default: nil)
		row = @items.fetch(args, []).to_a.first
		row = row.transform_keys(&:to_sym) if row && field_names_as == :symbol
		EMPromise.resolve(row || default)
	end

	def exec_defer(_, _)
		EMPromise.resolve(nil)
	end
end

class FakeMemcache
	def initialize(data={})
		@data = data
	end

	def set(k, v, _expires=nil)
		raise "No spaces" if k =~ /\s/

		@data[k] = v
	end

	def get(k)
		yield @data[k]
	end
end

class FakeLog
	def initialize
		@logs = []
	end

	def respond_to_missing?(*)
		true
	end

	def method_missing(*args)
		@logs << args
	end
end

class FakeIBRRepo
	def initialize(registrations={})
		@registrations = registrations
	end

	def registered?(jid, from:)
		@registrations.dig(jid.to_s, from.to_s) || false
	end
end

module EventMachine
	class << self
		# Patch EM.add_timer to be instant in tests
		alias old_add_timer add_timer
		def add_timer(*args, &block)
			args[0] = 0
			old_add_timer(*args, &block)
		end
	end
end

module Minitest
	class Test
		def self.property(m, &block)
			define_method("test_#{m}") do
				property_of(&block).check { |args| send(m, *args) }
			end
		end

		def self.em(m)
			alias_method "raw_#{m}", m
			define_method(m) do
				EM.run do
					Fiber.new {
						begin
							send("raw_#{m}")
						ensure
							EM.stop
						end
					}.resume
				end
			end
		end
	end
end
