extends SceneTree

const FishCatchType = preload("res://fish/fish_catch.gd")
const FishQualityType = preload("res://fish/fish_quality.gd")
const FishSelectorType = preload("res://fish/fish_selector.gd")
const CollectionLogType = preload("res://collection/collection_log.gd")
const NetworkSaleServiceType = preload(
	"res://network/network_sale_service.gd"
)
const Catalog: FishPool = preload("res://fish/pools/fish_catalog.tres")
const PelicanBuyer: FishBuyerProfile = preload(
	"res://economy/buyers/pelicans.tres"
)


func _initialize() -> void:
	call_deferred("_run")


func _run() -> void:
	_validate_tiers_and_distribution()
	_validate_barrier_challenge_curve()
	_validate_fight_pacing_and_reel_upgrades()
	_validate_catch_round_trip_and_sale()
	_validate_mail_round_trip()
	_validate_collection_mastery()
	_validate_version_four_migration()
	assert(NetworkProtocol.PROTOCOL_VERSION == 3)
	assert(NetworkProtocol.ENET_CHANNEL_COUNT == 10)
	assert(NetworkProtocol.FISH_QUALITY_CAPABILITY == "fish_quality_v1")
	print("Fish quality validation: PASS")
	quit()


func _validate_tiers_and_distribution() -> void:
	assert(FishQualityType.TIER_COUNT == 5)
	assert(FishQualityType.display_name(0) == "boring")
	assert(FishQualityType.display_name(4) == "shiny")
	assert(UIPalette.get_quality_color(0) == UIPalette.QUALITY_BORING)
	assert(UIPalette.get_quality_color(4) == UIPalette.QUALITY_SHINY)
	assert(
		UIPalette.get_quality_color(0)
		!= UIPalette.get_quality_color(1)
	)
	assert(FishQualityType.apply_sale_value(3, 0) == 3)
	assert(FishQualityType.apply_sale_value(3, 1) == 4)
	assert(FishQualityType.apply_sale_value(3, 2) == 5)
	assert(FishQualityType.apply_sale_value(3, 3) == 6)
	assert(FishQualityType.apply_sale_value(3, 4) == 7)
	var previous_offer: int = -1
	for quality: int in FishQualityType.TIER_COUNT:
		var offer: int = PelicanBuyer.get_quality_offer(3, quality)
		assert(offer > previous_offer)
		previous_offer = offer
	assert(
		FishQualityType.qualified_name_with_article("bluegill", 0)
		== "a boring bluegill"
	)
	assert(
		FishQualityType.qualified_name_with_article("bluegill", 1)
		== "an average bluegill"
	)
	var rng := RandomNumberGenerator.new()
	rng.seed = 727272
	var counts: Array[int] = [0, 0, 0, 0, 0]
	const SAMPLE_COUNT: int = 50000
	for _sample: int in SAMPLE_COUNT:
		counts[FishQualityType.roll(rng)] += 1
	for quality: int in FishQualityType.TIER_COUNT:
		var observed: float = float(counts[quality]) / float(SAMPLE_COUNT)
		var expected: float = (
			FishQualityType.BASE_ROLL_WEIGHTS[quality] / 100.0
		)
		assert(absf(observed - expected) < 0.015)


func _validate_barrier_challenge_curve() -> void:
	assert(
		FishQualityType.BARRIER_HEALTH_MULTIPLIERS.size()
		== FishQualityType.TIER_COUNT
	)
	var expected_minimums: Array[int] = [1, 10, 50, 100, 200]
	var expected_maximums: Array[int] = [9, 50, 100, 200, 400]
	var previous_health: int = 0
	for quality: int in FishQualityType.TIER_COUNT:
		var health: int = FishQualityType.apply_barrier_health(8, quality)
		assert(health > previous_health)
		previous_health = health
	assert(FishQualityType.apply_barrier_health(8, -1) == 8)
	assert(FishQualityType.apply_barrier_health(0, FishQualityType.Tier.SHINY) == 4)
	var impressive_low_input: int = FishQualityType.barrier_health_for_catch(
		FishQualityType.Tier.IMPRESSIVE,
		0,
		0.0,
		1.0,
	)
	var impressive_high_input: int = FishQualityType.barrier_health_for_catch(
		FishQualityType.Tier.IMPRESSIVE,
		4,
		1.0,
		1.0,
	)
	assert(impressive_low_input >= 50 and impressive_low_input <= 100)
	assert(impressive_high_input > impressive_low_input)
	assert(impressive_high_input <= 100)
	assert(
		FishQualityType.barrier_health_for_catch(
			FishQualityType.Tier.BORING,
			4,
			1.0,
			1.0,
		) <= 9
	)

	var profile := CatchDifficultyProfile.new()
	profile.barrier_count_min = 1
	profile.barrier_count_max = 1
	profile.barrier_health_min = 8
	profile.barrier_health_max = 8
	profile.first_barrier_margin = 0.2
	profile.final_barrier_margin = 0.2
	profile.minimum_barrier_spacing = 0.1
	var controller := CatchController.new()
	root.add_child(controller)
	for quality: int in FishQualityType.TIER_COUNT:
		controller.start_authoritative_encounter(
			profile,
			0.1,
			1,
			818181,
			quality,
		)
		var barriers_value: Variant = controller.get("_barriers")
		assert(barriers_value is Array)
		var barriers: Array = barriers_value as Array
		assert(barriers.size() == 1)
		var barrier := barriers[0] as RefCounted
		assert(barrier != null)
		var health: int = int(barrier.get("maximum_health"))
		assert(health >= expected_minimums[quality])
		assert(health <= expected_maximums[quality])
	controller.queue_free()

	var shiny_health: int = FishQualityType.barrier_health_for_catch(
		FishQualityType.Tier.SHINY,
		4,
		1.0,
		1.0,
	)
	var base_power_clicks: int = ceili(float(shiny_health) / 1.0)
	var max_power_clicks: int = ceili(
		float(shiny_health)
		/ float(PlayerFishingUpgrades.MAX_BARRIER_POWER_LEVEL + 1)
	)
	assert(base_power_clicks == 400)
	assert(max_power_clicks == 100)
	assert(max_power_clicks < base_power_clicks)


func _validate_fight_pacing_and_reel_upgrades() -> void:
	assert(is_equal_approx(CatchController.CHASE_SPEED, 0.07))
	assert(is_equal_approx(CatchController.CHASE_START_DELAY, 1.0))
	assert(is_equal_approx(CatchController.CHASE_START_OFFSET, 0.04))
	assert(is_equal_approx(Player.BASE_REEL_SPEED, 0.16))

	var upgrades := PlayerFishingUpgrades.new()
	assert(is_equal_approx(upgrades.get_reel_speed_multiplier(), 1.0))
	assert(
		upgrades.restore_levels(
			PlayerFishingUpgrades.MAX_REEL_SPEED_LEVEL,
			0,
		)
	)
	var upgraded_multiplier: float = upgrades.get_reel_speed_multiplier()
	assert(is_equal_approx(upgraded_multiplier, 1.5))

	var profile := CatchDifficultyProfile.new()
	profile.barrier_count_min = 0
	profile.barrier_count_max = 0
	var controller := CatchController.new()
	root.add_child(controller)
	controller.start_authoritative_encounter(
		profile,
		Player.BASE_REEL_SPEED,
		1,
		919191,
	)
	controller.set_reel_input(true)
	controller.call("_update_free_reeling", 1.0)
	var base_progress: float = controller.progress
	assert(is_equal_approx(base_progress, Player.BASE_REEL_SPEED))

	controller.reset()
	controller.start_authoritative_encounter(
		profile,
		Player.BASE_REEL_SPEED * upgraded_multiplier,
		1,
		919191,
	)
	controller.set_reel_input(true)
	controller.call("_update_free_reeling", 1.0)
	assert(
		is_equal_approx(
			controller.progress,
			Player.BASE_REEL_SPEED * upgraded_multiplier,
		)
	)
	assert(controller.progress > base_progress)
	controller.queue_free()
	upgrades.queue_free()


func _validate_catch_round_trip_and_sale() -> void:
	var fish: FishData = Catalog.get_fish_by_id(&"bluegill")
	assert(fish != null)
	var selector := FishSelectorType.new()
	selector.use_deterministic_test_seed = true
	selector.deterministic_test_seed = 9911
	selector.quality_weight_multipliers = [0.0, 0.0, 0.0, 0.0, 1.0]
	selector.begin_roll()
	var fish_catch: FishCatch = selector.create_catch(fish, [&"worm"])
	assert(fish_catch != null)
	assert(fish_catch.quality == FishQualityType.Tier.SHINY)
	assert(
		fish_catch.sale_value
		== FishQualityType.apply_sale_value(
			fish.get_sale_value_for_weight(fish_catch.weight_lb),
			FishQualityType.Tier.SHINY,
		)
	)
	var save_data: Dictionary = fish_catch.to_save_dict()
	fish_catch.catch_sequence = 1
	save_data = fish_catch.to_save_dict()
	var restored: FishCatch = FishCatchType.from_save_dict(save_data, fish)
	assert(restored != null and restored.quality == fish_catch.quality)
	var network_data: Dictionary = fish_catch.to_network_dict()
	var replicated: FishCatch = FishCatchType.from_network_dict(
		network_data,
		fish,
	)
	assert(replicated != null and replicated.quality == fish_catch.quality)
	var missing_quality: Dictionary = network_data.duplicate(true)
	missing_quality.erase("quality")
	assert(FishCatchType.from_network_dict(missing_quality, fish) == null)

	var session := NetworkSession.new()
	var sale_service := NetworkSaleServiceType.new()
	root.add_child(session)
	root.add_child(sale_service)
	sale_service.set("_session", session)
	sale_service.set("_fish_catalog", Catalog)
	var accepted: Dictionary = sale_service.call(
		"_build_authoritative_result",
		1,
		"quality_sale",
		[network_data],
		PelicanBuyer,
	)
	assert(bool(accepted.get("accepted", false)))
	assert(
		int(accepted["payout"])
		== PelicanBuyer.get_quality_offer(
			fish.get_sale_value_for_weight(fish_catch.weight_lb),
			fish_catch.quality,
		)
	)
	var inventory := FishInventory.new()
	var wallet := PlayerWallet.new()
	var local_sale := FishSaleService.new()
	root.add_child(inventory)
	root.add_child(wallet)
	root.add_child(local_sale)
	local_sale.setup(inventory, wallet)
	inventory.add_catch(fish_catch)
	var local_preview: FishSaleResult = local_sale.preview_batch(
		[fish_catch.catch_id],
		PelicanBuyer,
	)
	assert(local_preview.is_success())
	assert(local_preview.payout == int(accepted["payout"]))
	var forged: Dictionary = network_data.duplicate(true)
	forged["quality"] = FishQualityType.Tier.BORING
	var rejected: Dictionary = sale_service.call(
		"_build_authoritative_result",
		1,
		"quality_sale_forged",
		[forged],
		PelicanBuyer,
	)
	assert(not bool(rejected.get("accepted", false)))
	local_sale.queue_free()
	wallet.queue_free()
	inventory.queue_free()
	sale_service.queue_free()
	session.queue_free()


func _validate_mail_round_trip() -> void:
	var fish: FishData = Catalog.get_fish_by_id(&"bluegill")
	var fish_catch := FishCatchType.new()
	fish_catch.fish = fish
	fish_catch.fish_id = fish.id
	fish_catch.weight_lb = fish.get_minimum_weight()
	fish_catch.display_scale = fish.get_display_scale_for_weight(
		fish_catch.weight_lb
	)
	fish_catch.quality = FishQualityType.Tier.IMPRESSIVE
	fish_catch.sale_value = FishQualityType.apply_sale_value(
		fish.get_sale_value_for_weight(fish_catch.weight_lb),
		fish_catch.quality,
	)
	fish_catch.ensure_identity()
	var attachment: Dictionary = {
		"type": PlayerAssetReservationService.AttachmentType.FISH,
		"catch_id": String(fish_catch.catch_id),
		"catch": fish_catch.to_network_dict(),
	}
	var service := NetworkMailService.new()
	root.add_child(service)
	service.set("_fish_catalog", Catalog)
	var restored: FishCatch = service.call("_decode_catch", attachment)
	assert(restored != null)
	assert(restored.quality == FishQualityType.Tier.IMPRESSIVE)
	var signature: Array = NetworkMailProtocol.attachment_signature_fields(
		attachment
	)
	assert(int(signature[5]) == FishQualityType.Tier.IMPRESSIVE)
	var altered: Dictionary = attachment.duplicate(true)
	(altered["catch"] as Dictionary)["quality"] = (
		FishQualityType.Tier.BORING
	)
	assert(
		NetworkMailProtocol.attachment_signature_fields(altered)
		!= signature
	)
	service.queue_free()


func _validate_collection_mastery() -> void:
	var collection := CollectionLogType.new()
	root.add_child(collection)
	for quality: int in FishQualityType.TIER_COUNT:
		collection.mark_quality_discovered(&"bluegill", quality)
		assert(collection.has_discovered_quality(&"bluegill", quality))
	assert(collection.has_discovered(&"bluegill"))
	assert(collection.has_mastered(&"bluegill"))
	assert(
		collection.get_quality_mask(&"bluegill")
		== FishQualityType.ALL_TIERS_MASK
	)
	collection.queue_free()


func _validate_version_four_migration() -> void:
	var manager := PlayerSaveManager.new()
	root.add_child(manager)
	var version_four: Dictionary = {
		"save_version": 4,
		"wallet": {"balance": 12},
		"collection": {"discovered_fish_ids": ["bluegill"]},
		"inventory": {
			"next_catch_sequence": 2,
			"catches": [{
				"catch_id": "carp:legacy",
				"catch_sequence": 1,
				"fish_id": "carp",
				"weight_lb": 2.0,
				"display_scale": 1.0,
				"sale_value": 4,
				"is_favorited": false,
			}],
		},
		"bag": {"items": []},
		"hotbar": {"selected_slot": 0, "slots": []},
		"upgrades": {"reel_speed_level": 0, "barrier_power_level": 0},
		"cooler": {"capacity_level": 0},
	}
	var migrated: Dictionary = manager.call(
		"_migrate_save",
		version_four,
		4,
	)
	assert(int(migrated.get("save_version", -1)) == 7)
	assert(int((migrated["experience"] as Dictionary)["total_experience"]) == 0)
	assert(
		is_equal_approx(
			float((migrated["world"] as Dictionary)["time_hours"]),
			8.0,
		)
	)
	var catches: Array = migrated["inventory"]["catches"]
	assert(int((catches[0] as Dictionary)["quality"]) == 0)
	var collection: Dictionary = migrated["collection"]
	var ids: Array = collection["discovered_fish_ids"]
	assert("bluegill" in ids and "carp" in ids)
	var masks: Dictionary = collection["discovered_quality_masks"]
	var boring_bit: int = FishQualityType.bit_for(FishQualityType.Tier.BORING)
	assert(int(masks["bluegill"]) == boring_bit)
	assert(int(masks["carp"]) == boring_bit)
	assert(
		PlayerJobService.validate_save_data(migrated.get("jobs", {}))
	)
	manager.queue_free()
