# frozen_string_literal: true

require "pending_transaction_repo"

class PendingTransactionRepo
	def setup_mocks
		@redis = Minitest::Mock.new
		@electrum = Minitest::Mock.new
	end

	def override_filters(filters)
		@filters = filters
	end
end

FakeElectrumTransaction = Struct.new(:tx_hash, :confirmations, :value) {
	def amount_for(_addr)
		value
	end
}

class TestPendingTransactionRepo < Minitest::Test
	def test_empty_map
		repo = PendingTransactionRepo.new("key")
		repo.setup_mocks
		repo.redis.expect(
			:hgetall,
			[],
			["key"]
		)
		repo.map do |_pending, _customer_id|
			flunk "Shouldn't yield when empty"
		end

		assert_mock repo.redis
		assert_mock repo.electrum
	end

	def test_map
		repo = PendingTransactionRepo.new("key")
		repo.setup_mocks
		repo.override_filters([])
		repo.redis.expect(
			:hgetall,
			[["tx/addr", "1234"]],
			["key"]
		)
		repo.electrum.expect(
			:gettransaction,
			FakeElectrumTransaction.new("tx", 6, 0.5),
			["tx"]
		)

		v = repo.map { |pending, customer_id|
			"#{pending.value} #{customer_id}"
		}

		assert_equal ["0.5 1234"], v, "Should have returned result of block"

		assert_mock repo.redis
		assert_mock repo.electrum
	end

	def test_error_handler
		repo = PendingTransactionRepo.new("key")
		repo.setup_mocks
		repo.override_filters([])
		repo.redis.expect(
			:hgetall,
			[["tx/addr", "1234"], ["missing/addr", "1234"]],
			["key"]
		)
		def repo.electrum
			Class.new {
				def gettransaction(txid)
					if txid == "missing"
						raise Electrum::NoTransaction, "Couldn't find"
					end

					FakeElectrumTransaction.new("tx", 6, 0.5)
				end
			}.new
		end

		repo.error_handler do |e|
			case e
			when Electrum::NoTransaction
				true
			end
		end

		v = repo.map { |pending, customer_id|
			"#{pending.value} #{customer_id}"
		}

		assert_equal ["0.5 1234"], v, "Should have returned result of block"

		assert_mock repo.redis
	end

	def test_other_errors
		repo = PendingTransactionRepo.new("key")
		repo.setup_mocks
		repo.override_filters([])
		repo.redis.expect(
			:hgetall,
			[["tx/addr", "1234"], ["error/addr", "1234"]],
			["key"]
		)
		def repo.electrum
			Class.new {
				def gettransaction(txid)
					raise "Oh no" if txid == "error"

					FakeElectrumTransaction.new("tx", 6, 0.5)
				end
			}.new
		end

		repo.error_handler do |e|
			case e
			when Electrum::NoTransaction
				true
			end
		end

		assert_raises(RuntimeError) do
			repo.map { |pending, customer_id|
				"#{pending.value} #{customer_id}"
			}
		end

		assert_mock repo.redis
	end

	# This is basically the same as test_other_errors but uses the default
	# error handler that should re-throw everything
	def test_default_errors
		repo = PendingTransactionRepo.new("key")
		repo.setup_mocks
		repo.override_filters([])
		repo.redis.expect(
			:hgetall,
			[["tx/addr", "1234"], ["error/addr", "1234"]],
			["key"]
		)
		def repo.electrum
			Class.new {
				def gettransaction(txid)
					raise "Oh no" if txid == "error"

					FakeElectrumTransaction.new("tx", 6, 0.5)
				end
			}.new
		end

		assert_raises(RuntimeError) do
			repo.map { |pending, customer_id|
				"#{pending.value} #{customer_id}"
			}
		end

		assert_mock repo.redis
	end

	def test_remove_transaction
		repo = PendingTransactionRepo.new("key")
		repo.setup_mocks

		pending = PendingTransactionRepo::PendingTransaction.new(
			FakeElectrumTransaction.new("tx", 6, 0.5),
			"addr"
		)

		repo.redis.expect(:hdel, nil, ["key", "tx/addr"])

		repo.remove_transaction(pending)

		assert_mock repo.redis
		assert_mock repo.electrum
	end

	def test_mark_ignored
		repo = PendingTransactionRepo.new("key", ignored_key: "ig")
		repo.setup_mocks

		pending = PendingTransactionRepo::PendingTransaction.new(
			FakeElectrumTransaction.new("tx", 6, 0.5),
			"addr"
		)

		repo.redis.expect(:sadd, nil, ["ig", "tx/addr"])

		repo.mark_ignored(pending)

		assert_mock repo.redis
		assert_mock repo.electrum
	end

	def test_chunking
		repo = PendingTransactionRepo.new("key")
		repo.setup_mocks
		mock_filter = Minitest::Mock.new
		mock_filter.expect(
			:filter_chunk, [["one/a", "1234"], ["two/a", "1234"]],
			[[["one/a", "1234"], ["two/a", "1234"]]]
		)
		mock_filter.expect(
			:filter_chunk, [["three/a", "1234"]],
			[[["three/a", "1234"]]]
		)
		repo.override_filters([mock_filter])
		repo.redis.expect(
			:hgetall,
			[["one/a", "1234"], ["two/a", "1234"], ["three/a", "1234"]],
			["key"]
		)
		repo.electrum.expect(
			:gettransaction,
			FakeElectrumTransaction.new("one", 6, 0.5),
			["one"]
		)
		repo.electrum.expect(
			:gettransaction,
			FakeElectrumTransaction.new("two", 6, 0.5),
			["two"]
		)
		repo.electrum.expect(
			:gettransaction,
			FakeElectrumTransaction.new("three", 6, 0.5),
			["three"]
		)

		v = repo.map(chunk_size: 2) { |pending, _customer_id|
			pending.tx_hash
		}

		assert_equal(
			["one", "two", "three"], v,
			"Should have returned result of block"
		)

		assert_mock repo.redis
		assert_mock repo.electrum
		assert_mock mock_filter
	end

	def test_existing_transaction_filter
		db_mock = Minitest::Mock.new
		filter = PendingTransactionRepo::ExistingTransactionFilter.new(db_mock)

		db_mock.expect(:escape_string, "one/a", ["one/a"])
		db_mock.expect(:escape_string, "two/a", ["two/a"])
		db_mock.expect(:escape_string, "three/b", ["three/b"])
		# I've pretended this made a change here
		db_mock.expect(:escape_string, "four/C", ["four/c"])

		# I don't want to match the regex literally, that seems like a bit much
		# so instead I've just matched the parameterized part
		db_mock.expect(
			:exec_params,
			[
				{ "transaction_id" => "one/a", "exists" => false },
				{ "transaction_id" => "two/a", "exists" => true },
				{ "transaction_id" => "three/b", "exists" => true },
				{ "transaction_id" => "four/c", "exists" => false }
			],
			[/
				\(VALUES
				\s
				\('one\/a'\),\('two\/a'\),
				\('three\/b'\),\('four\/C'\)
				\)
			/x]
		)

		remaining = filter.filter_chunk([
			["one/a", "1234"],
			["two/a", "1234"],
			["three/b", "4321"],
			["four/c", "2323"]
		])

		assert_equal(
			[["one/a", "1234"], ["four/c", "2323"]],
			remaining,
			"should only include unfiltered results"
		)

		assert_mock db_mock
	end

	def test_ignored_transaction_filter
		redis = Object.new

		def redis.pipelined
			@stuff = []
			yield
			@stuff
		end

		def redis.sismember(key, txid)
			raise unless key == "key"

			@stuff << ["two/a", "three/b"].include?(txid)
		end

		filter = PendingTransactionRepo::IgnoredTransactionFilter.new(
			redis, "key"
		)

		remaining = filter.filter_chunk([
			["one/a", "1234"],
			["two/a", "1234"],
			["three/b", "4321"],
			["four/c", "2323"]
		])

		assert_equal(
			[["one/a", "1234"], ["four/c", "2323"]],
			remaining,
			"should only include unfiltered results"
		)
	end

	def test_wrong_customer_filter
		redis = Object.new

		def redis.pipelined
			@stuff = []
			yield
			@stuff
		end

		def redis.sismember(key, value)
			store = {
				"test_key_1234" => ["not_a"],
				"test_key_4321" => ["b"],
				"test_key_2323" => ["c"]
			}
			@stuff << store[key].include?(value)
		end

		filter = PendingTransactionRepo::WrongCustomerFilter.new(
			redis, ->(customer_id) { "test_key_#{customer_id}" }
		)

		def filter.warn(s)
			@warnings ||= []
			@warnings << s
		end

		def filter.sneak_warnings
			@warnings
		end

		remaining = filter.filter_chunk([
			["one/a", "1234"],
			["two/a", "1234"],
			["three/b", "4321"],
			["four/c", "2323"]
		])

		assert_equal(
			[["three/b", "4321"], ["four/c", "2323"]],
			remaining,
			"should only include unfiltered results"
		)

		assert_equal(
			[
				"one/a doesn't match customer 1234",
				"two/a doesn't match customer 1234"
			],
			filter.sneak_warnings,
			"should have warned about busted results"
		)
	end

	def test_filter_stack
		repo = PendingTransactionRepo.new("key")
		repo.setup_mocks
		mock_filter_one = Minitest::Mock.new
		mock_filter_one.expect(
			:filter_chunk, [["one/a", "1234"], ["two/a", "1234"]],
			[[["one/a", "1234"], ["two/a", "1234"], ["three/a", "1234"]]]
		)

		mock_filter_two = Minitest::Mock.new
		mock_filter_two.expect(
			:filter_chunk, [["one/a", "1234"]],
			[[["one/a", "1234"], ["two/a", "1234"]]]
		)

		repo.override_filters([mock_filter_one, mock_filter_two])
		repo.redis.expect(
			:hgetall,
			[["one/a", "1234"], ["two/a", "1234"], ["three/a", "1234"]],
			["key"]
		)
		repo.redis.expect(
			:hdel,
			2,
			["key", ["two/a", "three/a"]]
		)
		repo.electrum.expect(
			:gettransaction,
			FakeElectrumTransaction.new("one", 6, 0.5),
			["one"]
		)

		v = repo.map { |pending, _customer_id|
			pending.tx_hash
		}

		assert_equal(
			["one"], v,
			"Should have returned result of block"
		)

		assert_mock repo.redis
		assert_mock repo.electrum
		assert_mock mock_filter_one
		assert_mock mock_filter_two
	end

	def test_filter_all
		repo = PendingTransactionRepo.new("key")
		repo.setup_mocks
		mock_filter_one = Minitest::Mock.new
		mock_filter_one.expect(
			:filter_chunk, [],
			[[["one/a", "1234"], ["two/a", "1234"], ["three/a", "1234"]]]
		)

		# Shouldn't be called at all
		mock_filter_two = Minitest::Mock.new

		repo.override_filters([mock_filter_one, mock_filter_two])
		repo.redis.expect(
			:hgetall,
			[["one/a", "1234"], ["two/a", "1234"], ["three/a", "1234"]],
			["key"]
		)
		repo.redis.expect(
			:hdel,
			3,
			["key", ["one/a", "two/a", "three/a"]]
		)

		v = repo.map { |pending, _customer_id|
			pending.tx_hash
		}

		assert_equal(
			[], v,
			"Should have returned result of block"
		)

		assert_mock repo.redis
		assert_mock repo.electrum
		assert_mock mock_filter_one
		assert_mock mock_filter_two
	end
end
