extends SceneTree

const WorldTimeServiceType = preload("res://world/world_time_service.gd")
const WorldTimeVisualControllerType = preload(
	"res://world/world_time_visual_controller.gd"
)
const NetworkWorldTimeServiceType = preload(
	"res://network/network_world_time_service.gd"
)
const FishingContextType = preload("res://fishing/fishing_context.gd")
const FishDataType = preload("res://fish/fish_data.gd")
const FishPoolType = preload("res://fish/fish_pool.gd")
const FishingSpotType = preload("res://fishing/fishing_spot.gd")
const FishableWaterRegionType = preload(
	"res://world/fishable_water_region.gd"
)
const WeatherIconType = preload("res://ui/weather_icon.gd")

const Catalog: FishPoolType = preload("res://fish/pools/fish_catalog.tres")
const SaltWaterMaterial: ShaderMaterial = preload(
	"res://world/materials/stylized_water.tres"
)
const FreshWaterMaterial: ShaderMaterial = preload(
	"res://world/materials/stylized_water_fresh.tres"
)


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


func _run() -> void:
	_validate_clock_boundaries_and_duration()
	_validate_persistent_host_clock()
	_validate_fishing_availability()
	_validate_fishing_spot_context()
	_validate_network_snapshot_bounds()
	_validate_environment_presentation()
	_validate_weather_clock_icon()
	print("World time validation: PASS")
	quit()


func _validate_clock_boundaries_and_duration() -> void:
	assert(WorldTimeServiceType.REAL_SECONDS_PER_CYCLE == 3600.0)
	assert(
		WorldTimeServiceType.phase_for_hour(7.49)
		== WorldTimeServiceType.Phase.NIGHT
	)
	assert(
		WorldTimeServiceType.phase_for_hour(7.5)
		== WorldTimeServiceType.Phase.DAWN
	)
	assert(
		WorldTimeServiceType.phase_for_hour(8.49)
		== WorldTimeServiceType.Phase.DAWN
	)
	assert(
		WorldTimeServiceType.phase_for_hour(8.5)
		== WorldTimeServiceType.Phase.DAY
	)
	assert(
		WorldTimeServiceType.phase_for_hour(19.5)
		== WorldTimeServiceType.Phase.DUSK
	)
	assert(
		WorldTimeServiceType.phase_for_hour(20.5)
		== WorldTimeServiceType.Phase.NIGHT
	)
	assert(WorldTimeServiceType.format_clock_time(0.0) == "12:00 am")
	assert(WorldTimeServiceType.format_clock_time(8.0) == "8:00 am")
	assert(WorldTimeServiceType.format_clock_time(20.5) == "8:30 pm")

	var clock := WorldTimeServiceType.new()
	root.add_child(clock)
	clock.begin_session(8.0)
	clock.advance_time(1800.0)
	assert(is_equal_approx(clock.get_time_hours(), 20.0))
	assert(clock.is_night_period())
	assert(clock.is_transition())
	clock.advance_time(1800.0)
	assert(is_equal_approx(clock.get_time_hours(), 8.0))
	assert(not clock.is_night_period())
	assert(clock.is_transition())

	# Small high-refresh deltas must still advance the displayed minute. Using
	# is_equal_approx() per frame used to suppress every clock notification.
	var emitted_times: Array[float] = []
	clock.time_changed.connect(
		func(time_hours: float, _phase: WorldTimeService.Phase) -> void:
			emitted_times.append(time_hours)
	)
	clock.begin_session(12.0 + 31.0 / 60.0)
	emitted_times.clear()
	for _frame: int in 1201:
		clock.advance_time(1.0 / 240.0)
	assert(clock.get_clock_text() == "12:33 pm")
	assert(emitted_times.size() >= 2)
	clock.queue_free()


func _validate_persistent_host_clock() -> void:
	var clock := WorldTimeServiceType.new()
	root.add_child(clock)
	assert(clock.restore_persistent_time_hours(18.75))
	clock.set_persistence_tracking_enabled(true)
	clock.begin_session(clock.get_persistent_time_hours())
	assert(is_equal_approx(clock.get_time_hours(), 18.75))
	clock.synchronize_time(19.25)
	assert(is_equal_approx(clock.get_persistent_time_hours(), 19.25))
	clock.set_persistence_tracking_enabled(false)
	clock.synchronize_time(6.5)
	assert(is_equal_approx(clock.get_time_hours(), 6.5))
	assert(is_equal_approx(clock.get_persistent_time_hours(), 19.25))
	assert(not clock.restore_persistent_time_hours(-1.0))
	assert(not clock.restore_persistent_time_hours(24.0))
	clock.queue_free()


func _validate_fishing_availability() -> void:
	var day_context := FishingContextType.new()
	day_context.location_tags = [&"starter_pond"]
	day_context.is_night = false
	var night_context := FishingContextType.new()
	night_context.location_tags = [&"starter_pond"]
	night_context.is_night = true
	var transition_context := FishingContextType.new()
	transition_context.location_tags = [&"starter_pond"]
	transition_context.is_night = true
	transition_context.is_day_night_transition = true

	for fish_id: StringName in [&"bluegill", &"sunfish"]:
		var day_fish: FishDataType = Catalog.get_fish_by_id(fish_id)
		assert(day_fish.availability.is_available(day_context))
		assert(not day_fish.availability.is_available(night_context))
		assert(day_fish.availability.is_available(transition_context))
	for fish_id: StringName in [
		&"catfish_blue",
		&"catfish_channel",
		&"catfish_flathead",
		&"catfish_white",
	]:
		var night_fish: FishDataType = Catalog.get_fish_by_id(fish_id)
		assert(not night_fish.availability.is_available(day_context))
		assert(night_fish.availability.is_available(night_context))
		assert(night_fish.availability.is_available(transition_context))
	for fish_id: StringName in [
		&"bass",
		&"carp",
		&"tuna_albacore",
		&"tuna_bigeye",
		&"tuna_bluefin",
		&"tuna_skipjack",
		&"tuna_yellowfin",
		&"goby_round",
		&"salmon_atlantic",
		&"salmon_chum",
		&"salmon_coho",
		&"salmon_pink",
		&"salmon_sockeye",
	]:
		var all_time_fish: FishDataType = Catalog.get_fish_by_id(fish_id)
		assert(all_time_fish.availability.is_available(day_context))
		assert(all_time_fish.availability.is_available(night_context))
		assert(all_time_fish.availability.is_available(transition_context))


func _validate_fishing_spot_context() -> void:
	var clock := WorldTimeServiceType.new()
	clock.begin_session(20.25)
	var fishing_spot := FishingSpotType.new()
	fishing_spot.set("_world_time", clock)
	var region := FishableWaterRegionType.new()
	region.location_tags = [&"starter_pond"]
	region.water_type = WaterType.Type.FRESH_WATER
	var dusk_context: FishingContext = fishing_spot.build_network_context(region)
	assert(dusk_context.is_night)
	assert(dusk_context.is_day_night_transition)
	clock.synchronize_time(14.0)
	var day_context: FishingContext = fishing_spot.build_network_context(region)
	assert(not day_context.is_night)
	assert(not day_context.is_day_night_transition)
	region.free()
	fishing_spot.free()
	clock.free()


func _validate_network_snapshot_bounds() -> void:
	assert(NetworkWorldTimeServiceType.validate_snapshot({
		"session_id": "session",
		"time_hours": 8.25,
		"sequence": 1,
	}))
	assert(not NetworkWorldTimeServiceType.validate_snapshot({
		"session_id": "session",
		"time_hours": 24.0,
		"sequence": 1,
	}))
	assert(not NetworkWorldTimeServiceType.validate_snapshot({
		"session_id": "",
		"time_hours": 8.0,
		"sequence": 1,
	}))


func _validate_environment_presentation() -> void:
	var world_root := Node3D.new()
	root.add_child(world_root)
	var world_environment := WorldEnvironment.new()
	var environment := Environment.new()
	var sky := Sky.new()
	var sky_material := ShaderMaterial.new()
	sky_material.shader = preload(
		"res://world/environment/netfishing_sky.gdshader"
	)
	sky.sky_material = sky_material
	environment.sky = sky
	environment.adjustment_enabled = true
	environment.fog_enabled = true
	world_environment.environment = environment
	world_root.add_child(world_environment)
	var sun := DirectionalLight3D.new()
	world_root.add_child(sun)
	var clock := WorldTimeServiceType.new()
	world_root.add_child(clock)
	var visuals := WorldTimeVisualControllerType.new()
	world_root.add_child(visuals)
	visuals.setup(clock, world_environment, sun)
	visuals.apply_time_immediately(12.0)
	var runtime_environment: Environment = world_environment.environment
	var runtime_sky_material := (
		runtime_environment.sky.sky_material as ShaderMaterial
	)
	var day_horizon_value: Variant = runtime_sky_material.get_shader_parameter(
		"sky_horizon_color"
	)
	assert(day_horizon_value is Color)
	var day_horizon: Color = day_horizon_value as Color
	var day_sun_visibility: float = float(
		runtime_sky_material.get_shader_parameter("sun_visibility")
	)
	var day_sun_direction: Vector3 = (
		runtime_sky_material.get_shader_parameter("sun_direction") as Vector3
	)
	assert(day_sun_visibility > 0.99)
	assert(day_sun_direction.y > 0.85)
	assert(
		float(runtime_sky_material.get_shader_parameter("moon_visibility"))
		< 0.01
	)
	var day_ambient_energy: float = runtime_environment.ambient_light_energy
	var day_water_brightness := float(
		SaltWaterMaterial.get_shader_parameter("environment_brightness")
	)
	assert(day_water_brightness > 0.99)
	assert(is_equal_approx(
		float(FreshWaterMaterial.get_shader_parameter("environment_brightness")),
		day_water_brightness,
	))
	visuals.apply_time_immediately(0.0)
	var night_horizon: Color = runtime_sky_material.get_shader_parameter(
		"sky_horizon_color"
	) as Color
	assert(night_horizon != day_horizon)
	assert(
		float(runtime_sky_material.get_shader_parameter("sun_visibility"))
		< 0.01
	)
	var night_moon_visibility: float = float(
		runtime_sky_material.get_shader_parameter("moon_visibility")
	)
	var night_moon_direction: Vector3 = (
		runtime_sky_material.get_shader_parameter("moon_direction") as Vector3
	)
	var night_sun_direction: Vector3 = (
		runtime_sky_material.get_shader_parameter("sun_direction") as Vector3
	)
	assert(night_moon_visibility > 0.99)
	assert(night_moon_direction.y > 0.85)
	assert(night_moon_direction.is_equal_approx(-night_sun_direction))
	assert(runtime_environment.ambient_light_energy < day_ambient_energy)
	var night_water_brightness := float(
		SaltWaterMaterial.get_shader_parameter("environment_brightness")
	)
	assert(night_water_brightness < day_water_brightness * 0.4)
	assert(is_equal_approx(
		float(FreshWaterMaterial.get_shader_parameter("environment_brightness")),
		night_water_brightness,
	))
	var night_water_tint := (
		SaltWaterMaterial.get_shader_parameter("environment_tint") as Color
	)
	assert(night_water_tint.get_luminance() < 0.5)
	var night_background_energy := runtime_environment.background_energy_multiplier
	var night_adjustment_brightness := runtime_environment.adjustment_brightness
	var night_adjustment_saturation := runtime_environment.adjustment_saturation
	visuals.apply_weather_immediately(WorldWeatherService.Weather.FOGGY)
	visuals.apply_time_immediately(0.0)
	var foggy_sky_horizon := (
		runtime_sky_material.get_shader_parameter("sky_horizon_color") as Color
	)
	assert(foggy_sky_horizon.is_equal_approx(night_horizon))
	assert(is_zero_approx(runtime_environment.fog_sky_affect))
	assert(is_equal_approx(runtime_environment.fog_depth_begin, 3.0))
	assert(is_equal_approx(runtime_environment.fog_depth_end, 28.0))
	assert(is_equal_approx(
		float(SaltWaterMaterial.get_shader_parameter("environment_brightness")),
		night_water_brightness,
	))
	assert(is_equal_approx(
		runtime_environment.background_energy_multiplier,
		night_background_energy,
	))
	assert(is_equal_approx(
		runtime_environment.adjustment_brightness,
		night_adjustment_brightness,
	))
	assert(is_equal_approx(
		runtime_environment.adjustment_saturation,
		night_adjustment_saturation,
	))
	visuals.apply_weather_immediately(WorldWeatherService.Weather.SUNNY)
	visuals.apply_time_immediately(20.0)
	var dusk_horizon: Color = runtime_sky_material.get_shader_parameter(
		"sky_horizon_color"
	) as Color
	assert(dusk_horizon.r > day_horizon.r)
	world_root.queue_free()


func _validate_weather_clock_icon() -> void:
	var weather_icon := WeatherIconType.new()
	root.add_child(weather_icon)
	assert(weather_icon.tooltip_text == "clear")
	assert(not weather_icon.is_nighttime())
	weather_icon.set_nighttime(true)
	assert(weather_icon.is_nighttime())
	weather_icon.set_weather(WorldWeatherService.Weather.CLOUDY)
	assert(weather_icon.tooltip_text == "cloudy")
	weather_icon.queue_free()
