package tests

import "core:fmt"
import "core:os"
import "core:strings"
import "core:testing"
import "../src/adapters"
import "../src/core"
import "../src/gui"
import "../src/shared"
import "../src/ui"

@test
gui_parse_autosave_interval_bounds :: proc(t: ^testing.T) {
	testing.expect(t, gui.parse_autosave_interval("42", 20) == 42, "valid interval should parse")
	testing.expect(t, gui.parse_autosave_interval("", 20) == 20, "empty interval should fallback to default")
	testing.expect(t, gui.parse_autosave_interval("1", 20) == 5, "interval should clamp to min")
	testing.expect(t, gui.parse_autosave_interval("999", 20) == 300, "interval should clamp to max")
}

@test
gui_fit_text_for_width_truncates_with_ellipsis :: proc(t: ^testing.T) {
	truncated := gui.fit_text_for_width("ABCDE", 1, 10)
	testing.expect(t, truncated == "ABC…", "fit_text_for_width should enforce 4-char minimum width rule and truncate with ellipsis")
}

@test
gui_fit_text_for_width_passthrough_cases :: proc(t: ^testing.T) {
	testing.expect(t, gui.fit_text_for_width("ABCD", 1, 10) == "ABCD", "text at min width should pass through")
	testing.expect(t, gui.fit_text_for_width("ABCDE", 80, 0) == "ABCDE", "non-positive px_per_char should bypass truncation")
}

@test
gui_set_autosave_interval_text_clamps_and_formats :: proc(t: ^testing.T) {
	interval_text := ""
	msg := gui.set_autosave_interval_text(&interval_text, 3)
	defer delete(interval_text)
	defer delete(msg)
	testing.expect(t, interval_text == "5", "text should clamp to minimum interval")
	testing.expect(t, strings.contains(msg, "5s"), "message should include clamped seconds")
}

@test
gui_log_view_toggle_helpers :: proc(t: ^testing.T) {
	lines: i32 = 6
	msg1 := gui.toggle_log_lines_with_message(&lines)
	defer delete(msg1)
	testing.expect(t, lines == 4, "log lines should toggle from 6 to 4")
	testing.expect(t, msg1 == "Log lines: 4", "log lines message should match")

	msg2 := gui.toggle_log_lines_with_message(&lines)
	defer delete(msg2)
	testing.expect(t, lines == 6, "log lines should toggle back to 6")
	testing.expect(t, msg2 == "Log lines: 6", "log lines message should match")

	oldest_first := false
	msg3 := gui.toggle_log_order_with_message(&oldest_first)
	defer delete(msg3)
	testing.expect(t, oldest_first, "log order should toggle to oldest-first")
	testing.expect(t, strings.contains(msg3, "oldest"), "log order message should mention oldest")

	msg4 := gui.toggle_log_order_with_message(&oldest_first)
	defer delete(msg4)
	testing.expect(t, !oldest_first, "log order should toggle back to newest-first")
	testing.expect(t, strings.contains(msg4, "newest"), "log order message should mention newest")
}

@test
gui_export_format_helper_updates_path_and_dirty :: proc(t: ^testing.T) {
	export_format: core.Export_Format = .PDF
	export_path := "./comic.pdf"
	is_dirty := false

	msg := gui.set_export_format_with_message(&export_format, &export_path, .CBZ, &is_dirty)
	defer delete(export_path)
	defer delete(msg)
	testing.expect(t, export_format == .CBZ, "format should update to CBZ")
	testing.expect(t, is_dirty, "format switch should mark state dirty")
	testing.expect(t, strings.has_suffix(export_path, ".cbz"), "export path should match selected format")
	testing.expect(t, strings.has_prefix(msg, "Export format: CBZ"), "status should include format label")
}

@test
gui_diagnostics_context_builder_maps_fields :: proc(t: ^testing.T) {
	state := core.new_initial_state()
	controller := ui.new_controller(state)
	defer ui.dispose_controller(&controller)

	action_log: gui.Action_Log
	ctx := gui.make_diagnostics_action_context(&controller, &action_log, true, false, true, false, 30, "./p.comic.json", "./e.cbz", 4, true)

	testing.expect(t, ctx.controller == &controller, "context should hold controller pointer")
	testing.expect(t, ctx.action_log == &action_log, "context should hold action-log pointer")
	testing.expect(t, ctx.is_dirty, "context should map dirty flag")
	testing.expect(t, !ctx.autosave_enabled, "context should map autosave flag")
	testing.expect(t, ctx.project_ok && !ctx.export_ok, "context should map path health flags")
	testing.expect(t, ctx.autosave_secs == 30, "context should map autosave seconds")
	testing.expect(t, ctx.project_path == "./p.comic.json" && ctx.export_path == "./e.cbz", "context should map paths")
	testing.expect(t, ctx.log_show_lines == 4 && ctx.log_oldest_first, "context should map log settings")
}

@test
gui_project_path_normalization_variants :: proc(t: ^testing.T) {
	p1 := gui.normalize_project_path("")
	testing.expect(t, p1 == "./gui_project.comic.json", "empty path should use default project path")

	p2 := gui.normalize_project_path("my_story")
	defer delete(p2)
	testing.expect(t, p2 == "my_story.comic.json", "bare name should append .comic.json")

	p3 := gui.normalize_project_path("my_story.json")
	defer delete(p3)
	testing.expect(t, p3 == "my_story.comic.json", "json path should be converted to .comic.json")

	p4 := gui.normalize_project_path("already.comic.json")
	testing.expect(t, p4 == "already.comic.json", "already-normalized path should remain unchanged")
}

@test
gui_fix_all_paths_normalizes_project_and_export :: proc(t: ^testing.T) {
	project_path := "project"
	export_path := "./out"
	defer delete(project_path)
	defer delete(export_path)

	gui.fix_all_paths(&project_path, &export_path, .PNG)
	testing.expect(t, strings.has_suffix(project_path, ".comic.json"), "fix_all_paths should normalize project suffix")
	testing.expect(t, strings.has_suffix(export_path, ".zip"), "PNG export should normalize to .zip suffix")
}

@test
gui_summary_toggle_supported_screen_guard :: proc(t: ^testing.T) {
	opts := gui.Summary_View_Options{}

	msg1 := gui.toggle_summary_show_if_supported(.Story, &opts)
	testing.expect(t, len(msg1) == 0, "show toggle should no-op on unsupported screen")

	msg2 := gui.toggle_summary_sort_if_supported(.Story, &opts)
	testing.expect(t, len(msg2) == 0, "sort toggle should no-op on unsupported screen")

	msg3 := gui.toggle_summary_show_if_supported(.Script, &opts)
	defer delete(msg3)
	testing.expect(t, strings.has_prefix(msg3, "Script summary show-all"), "show toggle should produce script status message")
}

@test
gui_push_dirty_status_sets_dirty_and_logs :: proc(t: ^testing.T) {
	is_dirty := false
	status_msg := fmt.aprintf("")
	action_log: gui.Action_Log
	defer gui.action_log_dispose(&action_log)
	defer delete(status_msg)

	gui.push_dirty_status(&is_dirty, &status_msg, &action_log, "Changed value")
	testing.expect(t, is_dirty, "push_dirty_status should set dirty flag")
	testing.expect(t, status_msg == "Changed value", "push_dirty_status should set status message")
	testing.expect(t, action_log.count == 1, "push_dirty_status should append one action-log entry")
}

@test
gui_push_status_if_nonempty_only_pushes_for_content :: proc(t: ^testing.T) {
	status_msg := fmt.aprintf("initial")
	action_log: gui.Action_Log
	defer gui.action_log_dispose(&action_log)
	defer delete(status_msg)

	gui.push_status_if_nonempty(&status_msg, &action_log, "")
	testing.expect(t, status_msg == "initial", "empty optional status should not overwrite status message")
	testing.expect(t, action_log.count == 0, "empty optional status should not push to action log")

	gui.push_status_if_nonempty(&status_msg, &action_log, "non-empty")
	testing.expect(t, status_msg == "non-empty", "non-empty optional status should update status message")
	testing.expect(t, action_log.count == 1, "non-empty optional status should append one action-log entry")
}

@test
gui_confirmation_request_sets_overlay_state :: proc(t: ^testing.T) {
	show_confirm_overlay := false
	show_help_overlay := true
	pending_action: gui.Pending_Confirm_Action = .None

	msg := gui.request_confirmation(&show_confirm_overlay, &show_help_overlay, &pending_action, .Open_Project, "Confirm open?")
	testing.expect(t, msg == "Confirm open?", "request_confirmation should return prompt message")
	testing.expect(t, show_confirm_overlay, "request_confirmation should enable confirm overlay")
	testing.expect(t, !show_help_overlay, "request_confirmation should close help overlay")
	testing.expect(t, pending_action == .Open_Project, "request_confirmation should set pending action")
}

@test
gui_autosave_tick_noop_when_disabled_or_clean :: proc(t: ^testing.T) {
	state := core.new_initial_state()
	project_path := "./tmp-test.comic.json"
	is_dirty := true
	last_autosave_at: f64 = 0
	last_save_at: f64 = -1

	msg1 := gui.autosave_tick_with_message(&project_path, state, false, &is_dirty, &last_autosave_at, &last_save_at, 1)
	testing.expect(t, len(msg1) == 0, "autosave tick should no-op when autosave is disabled")
	testing.expect(t, is_dirty, "autosave disabled should not mutate dirty flag")

	is_dirty = false
	msg2 := gui.autosave_tick_with_message(&project_path, state, true, &is_dirty, &last_autosave_at, &last_save_at, 1)
	testing.expect(t, len(msg2) == 0, "autosave tick should no-op when state is clean")
}

@test
gui_log_clear_and_reset_helpers :: proc(t: ^testing.T) {
	action_log: gui.Action_Log
	defer gui.action_log_dispose(&action_log)
	status_msg := fmt.aprintf("")
	defer delete(status_msg)

	gui.push_status(&status_msg, &action_log, "first")
	gui.push_status(&status_msg, &action_log, "second")
	testing.expect(t, action_log.count == 2, "setup should add log entries")

	clear_msg := gui.clear_action_log_with_message(&action_log)
	testing.expect(t, clear_msg == "Action log cleared", "clear helper should return expected message")
	testing.expect(t, action_log.count == 0, "clear helper should reset action log count")

	lines: i32 = 4
	oldest_first := true
	reset_msg := gui.reset_log_view_with_message(&lines, &oldest_first)
	testing.expect(t, reset_msg == "Reset log view", "reset helper should return expected message")
	testing.expect(t, lines == 6, "reset helper should restore default line count")
	testing.expect(t, !oldest_first, "reset helper should restore newest-first ordering")
}

@test
gui_confirm_resolve_none_returns_message :: proc(t: ^testing.T) {
	state := core.new_initial_state()
	controller := ui.new_controller(state)
	defer ui.dispose_controller(&controller)

	project_path := "./p.comic.json"
	export_path := "./e.pdf"
	is_dirty := false
	last_autosave_at: f64 = 0

	msg := gui.resolve_confirm_action_with_message(.None, &controller, &project_path, &export_path, .PDF, &is_dirty, &last_autosave_at)
	testing.expect(t, msg == "No pending destructive action", "resolve helper should report none-pending message")
}

@test
gui_help_overlay_toggle_and_close_helpers :: proc(t: ^testing.T) {
	show_help_overlay := false
	gui.toggle_help_overlay(&show_help_overlay)
	testing.expect(t, show_help_overlay, "toggle helper should enable help overlay")

	gui.close_help_overlay_if_open(&show_help_overlay)
	testing.expect(t, !show_help_overlay, "close helper should disable help overlay when open")

	gui.close_help_overlay_if_open(&show_help_overlay)
	testing.expect(t, !show_help_overlay, "close helper should be a no-op when already closed")
}

@test
gui_export_path_preset_and_derivation_helpers :: proc(t: ^testing.T) {
	export_path := ""
	msg1 := gui.set_export_preset_with_message(&export_path, .PNG)
	defer delete(msg1)
	testing.expect(t, strings.has_suffix(export_path, ".zip"), "preset helper should set PNG-compatible export path")
	testing.expect(t, strings.has_prefix(msg1, "Preset export path:"), "preset helper should return status message")

	delete(export_path)
	project_path := "./work/my.comic.json"
	msg2 := gui.set_export_path_from_project_with_message(&export_path, project_path, .CBZ)
	defer delete(export_path)
	defer delete(msg2)
	testing.expect(t, strings.has_suffix(export_path, ".cbz"), "project-derivation helper should set CBZ-compatible export path")
	testing.expect(t, strings.contains(msg2, "Export path from project dir:"), "project-derivation helper should return status message")

	msg3 := gui.set_project_path_from_export_with_message(&project_path, export_path)
	defer delete(project_path)
	defer delete(msg3)
	testing.expect(t, strings.has_suffix(project_path, "gui_project.comic.json"), "export-derivation helper should set project filename")
	testing.expect(t, strings.contains(msg3, "Project path from export dir:"), "export-derivation helper should return status message")
}

@test
gui_reset_project_session_resets_dirty_and_screen :: proc(t: ^testing.T) {
	state := core.new_initial_state()
	state.story_idea = "changed"
	controller := ui.new_controller(state)
	defer ui.dispose_controller(&controller)

	controller.active_screen = .Export
	is_dirty := true
	last_autosave_at: f64 = 123

	msg := gui.reset_project_session(&controller, &is_dirty, &last_autosave_at, false)
	testing.expect(t, msg == "Reset project", "reset helper should return expected status")
	testing.expect(t, !is_dirty, "reset helper should clear dirty flag")
	testing.expect(t, controller.active_screen == .Story, "reset helper should sync active screen to initial workflow")
	testing.expect(t, last_autosave_at == 123, "reset helper should not touch autosave timestamp when touch_time is false")
}

@test
gui_toggle_autosave_with_message_flips_state :: proc(t: ^testing.T) {
	autosave_enabled := true
	msg1 := gui.toggle_autosave_with_message(&autosave_enabled)
	defer delete(msg1)
	testing.expect(t, !autosave_enabled, "autosave toggle should flip from enabled to disabled")
	testing.expect(t, strings.contains(msg1, "no"), "autosave toggle message should report disabled state")

	msg2 := gui.toggle_autosave_with_message(&autosave_enabled)
	defer delete(msg2)
	testing.expect(t, autosave_enabled, "autosave toggle should flip back to enabled")
	testing.expect(t, strings.contains(msg2, "yes"), "autosave toggle message should report enabled state")
}

@test
gui_reset_helper_fields_with_message_sets_defaults :: proc(t: ^testing.T) {
	export_path := "./custom.cbz"
	local_script_pages := "9"
	autosave_interval_text := "90"
	defer delete(export_path)

	msg := gui.reset_helper_fields_with_message(&export_path, &local_script_pages, &autosave_interval_text, .PDF)
	testing.expect(t, msg == "Reset helper fields to defaults", "reset helpers should return expected status")
	testing.expect(t, export_path == "./comic.pdf", "reset helpers should restore format-based export default")
	testing.expect(t, local_script_pages == "2", "reset helpers should restore default local script pages")
	testing.expect(t, autosave_interval_text == "20", "reset helpers should restore default autosave interval text")
}

@test
gui_clear_selected_field_with_message_behaviors :: proc(t: ^testing.T) {
	state := core.new_initial_state()
	state.story_idea = "idea"
	export_path := "./comic.pdf"
	local_script_pages := "3"
	project_path := "./p.comic.json"
	autosave_interval_text := "30"
	is_dirty := false

	msg1 := gui.clear_selected_field_with_message(0, &state, &export_path, &local_script_pages, &project_path, &autosave_interval_text, &is_dirty)
	testing.expect(t, msg1 == "Cleared selected field", "clear helper should report clear when field had content")
	testing.expect(t, len(state.story_idea) == 0, "clear helper should clear selected text field")
	testing.expect(t, is_dirty, "clear helper should mark dirty when a field was cleared")

	is_dirty = false
	msg2 := gui.clear_selected_field_with_message(0, &state, &export_path, &local_script_pages, &project_path, &autosave_interval_text, &is_dirty)
	testing.expect(t, msg2 == "Selected field already empty", "clear helper should report already-empty state")
	testing.expect(t, !is_dirty, "clear helper should not mark dirty when nothing changed")
}

@test
gui_path_health_hint_variants :: proc(t: ^testing.T) {
	testing.expect(t, gui.path_health_hint(true, true) == "", "path-health hint should be empty when both paths are healthy")
	testing.expect(t, strings.contains(gui.path_health_hint(false, false), "Fix paths"), "path-health hint should mention fixing both paths")
	testing.expect(t, strings.contains(gui.path_health_hint(false, true), "Fix project path"), "path-health hint should mention project path fix")
	testing.expect(t, strings.contains(gui.path_health_hint(true, false), "Fix export path"), "path-health hint should mention export path fix")
}

@test
gui_path_health_predicates :: proc(t: ^testing.T) {
	testing.expect(t, gui.project_path_is_normalized("./x.comic.json"), "project path predicate should accept normalized path")
	testing.expect(t, !gui.project_path_is_normalized("./x.json"), "project path predicate should reject non-comic suffix")

	testing.expect(t, gui.export_path_matches_format("./x.pdf", .PDF), "export path predicate should accept PDF suffix")
	testing.expect(t, !gui.export_path_matches_format("./x.cbz", .PDF), "export path predicate should reject wrong format suffix")
}

@test
gui_screen_label_and_navigation_status :: proc(t: ^testing.T) {
	testing.expect(t, gui.screen_status_label(.Panels) == "Panels", "screen label helper should map enum to name")

	state := core.new_initial_state()
	controller := ui.new_controller(state)
	defer ui.dispose_controller(&controller)

	msg := gui.navigate_screen_with_status(&controller, .Story)
	defer delete(msg)
	testing.expect(t, msg == "Screen: Story", "navigate helper should return status message for successful navigation")
}

@test
gui_action_log_ring_buffer_retains_recent_entries :: proc(t: ^testing.T) {
	status_msg := fmt.aprintf("")
	action_log: gui.Action_Log
	defer gui.action_log_dispose(&action_log)
	defer delete(status_msg)

	for i in 0..<10 {
		msg := ""
		switch i {
		case 0: msg = "entry_00"
		case 1: msg = "entry_01"
		case 2: msg = "entry_02"
		case 3: msg = "entry_03"
		case 4: msg = "entry_04"
		case 5: msg = "entry_05"
		case 6: msg = "entry_06"
		case 7: msg = "entry_07"
		case 8: msg = "entry_08"
		case 9: msg = "entry_09"
		}
		gui.push_status(&status_msg, &action_log, msg)
	}

	testing.expect(t, action_log.count == 10, "action log should track total pushes")
	snapshot := gui.build_action_log_snapshot(action_log)
	defer delete(snapshot)
	testing.expect(t, strings.contains(snapshot, "entry_09"), "snapshot should include newest entry")
	testing.expect(t, !strings.contains(snapshot, "entry_00"), "snapshot should drop entries outside ring capacity")
}

@test
gui_action_log_clear_resets_count_and_snapshot :: proc(t: ^testing.T) {
	status_msg := fmt.aprintf("")
	action_log: gui.Action_Log
	defer gui.action_log_dispose(&action_log)
	defer delete(status_msg)

	gui.push_status(&status_msg, &action_log, "a")
	gui.push_status(&status_msg, &action_log, "b")
	testing.expect(t, action_log.count == 2, "setup should create action-log entries")
	action_log.last_push_at = 42

	_ = gui.clear_action_log_with_message(&action_log)
	testing.expect(t, action_log.count == 0, "clear helper should reset count")
	testing.expect(t, action_log.last_push_at == 0, "clear helper should reset last-push timestamp")
	snapshot := gui.build_action_log_snapshot(action_log)
	defer delete(snapshot)
	testing.expect(t, snapshot == "(action log empty)", "snapshot should report empty log after clear")
}

@test
gui_set_status_replaces_owned_string :: proc(t: ^testing.T) {
	status_msg := fmt.aprintf("start")
	defer delete(status_msg)

	gui.set_status(&status_msg, "updated")
	testing.expect(t, status_msg == "updated", "set_status should replace existing status text")
}

@test
gui_open_project_session_missing_file_returns_error_and_keeps_state :: proc(t: ^testing.T) {
	state := core.new_initial_state()
	state.story_idea = "original"
	controller := ui.new_controller(state)
	defer ui.dispose_controller(&controller)

	project_path := "./definitely_missing_project"
	export_path := "./comic.pdf"
	is_dirty := true
	last_autosave_at: f64 = 777

	msg := gui.open_project_session(&controller, &project_path, &export_path, .PDF, &is_dirty, &last_autosave_at)
	defer delete(project_path)
	testing.expect(t, strings.contains(msg, "project file does not exist"), "open helper should surface missing-file error")
	testing.expect(t, strings.has_suffix(project_path, ".comic.json"), "open helper should normalize missing-file project path")
	testing.expect(t, controller.state.story_idea == "original", "failed open should keep existing controller state")
	testing.expect(t, is_dirty, "failed open should preserve dirty flag")
	testing.expect(t, last_autosave_at == 777, "failed open should preserve autosave timestamp")
}

@test
gui_resolve_confirm_action_reset_branch :: proc(t: ^testing.T) {
	state := core.new_initial_state()
	state.story_idea = "before"
	controller := ui.new_controller(state)
	defer ui.dispose_controller(&controller)

	project_path := "./unused.comic.json"
	export_path := "./unused.pdf"
	is_dirty := true
	last_autosave_at: f64 = 42

	msg := gui.resolve_confirm_action_with_message(.Reset_Project, &controller, &project_path, &export_path, .PDF, &is_dirty, &last_autosave_at)
	testing.expect(t, msg == "Reset project", "resolve helper should dispatch reset action")
	testing.expect(t, !is_dirty, "reset dispatch should clear dirty flag")
	testing.expect(t, controller.active_screen == .Story, "reset dispatch should sync active screen")
	testing.expect(t, last_autosave_at >= 0, "reset dispatch should set a non-negative autosave timestamp")
	testing.expect(t, last_autosave_at != 42, "reset dispatch should refresh autosave timestamp")
}

@test
gui_open_project_session_success_updates_state_and_paths :: proc(t: ^testing.T) {
	tmp_dir, terr := os.make_directory_temp("", "comic-gui-open-*", context.temp_allocator)
	testing.expect(t, terr == nil, "temp dir should be created")
	if terr != nil {
		return
	}
	defer os.remove_all(tmp_dir)

	project_path := fmt.aprintf("%s/session.comic.json", tmp_dir)
	defer delete(project_path)

	saved := core.new_initial_state()
	saved.story_idea = "loaded-story"
	err := adapters.save_project(project_path, saved)
	testing.expect(t, shared.is_ok(err), "save_project should succeed for open-session test")
	if !shared.is_ok(err) {
		return
	}

	controller := ui.new_controller(core.new_initial_state())
	defer ui.dispose_controller_owned(&controller)
	controller.state.story_idea = "original-story"

	export_path := "./placeholder.pdf"
	is_dirty := true
	last_autosave_at: f64 = 17

	msg := gui.open_project_session(&controller, &project_path, &export_path, .CBZ, &is_dirty, &last_autosave_at)
	defer delete(msg)
	defer delete(export_path)

	testing.expect(t, strings.has_prefix(msg, "Opened project:"), "open helper should return opened-project status")
	testing.expect(t, controller.state.story_idea == "loaded-story", "open helper should replace controller state from loaded project")
	testing.expect(t, !is_dirty, "open helper should clear dirty flag on success")
	testing.expect(t, strings.has_suffix(export_path, ".cbz"), "open helper should sync export path suffix to selected format")
	testing.expect(t, strings.contains(export_path, tmp_dir), "open helper should sync export path into the project directory")
	testing.expect(t, last_autosave_at != 17, "open helper should refresh autosave timestamp on success")
}

@test
gui_resolve_confirm_action_open_branch_success :: proc(t: ^testing.T) {
	tmp_dir, terr := os.make_directory_temp("", "comic-gui-confirm-open-*", context.temp_allocator)
	testing.expect(t, terr == nil, "temp dir should be created")
	if terr != nil {
		return
	}
	defer os.remove_all(tmp_dir)

	project_path := fmt.aprintf("%s/session.comic.json", tmp_dir)
	defer delete(project_path)

	saved := core.new_initial_state()
	saved.story_idea = "confirm-open-loaded"
	err := adapters.save_project(project_path, saved)
	testing.expect(t, shared.is_ok(err), "save_project should succeed for resolve-open test")
	if !shared.is_ok(err) {
		return
	}

	controller := ui.new_controller(core.new_initial_state())
	defer ui.dispose_controller_owned(&controller)
	controller.state.story_idea = "before-open"

	export_path := "./placeholder.pdf"
	is_dirty := true
	last_autosave_at: f64 = 99

	msg := gui.resolve_confirm_action_with_message(.Open_Project, &controller, &project_path, &export_path, .PDF, &is_dirty, &last_autosave_at)
	defer delete(msg)
	defer delete(export_path)

	testing.expect(t, strings.has_prefix(msg, "Opened project:"), "resolve helper should dispatch open action")
	testing.expect(t, controller.state.story_idea == "confirm-open-loaded", "resolve open dispatch should load target state")
	testing.expect(t, !is_dirty, "resolve open dispatch should clear dirty flag")
	testing.expect(t, strings.has_suffix(export_path, ".pdf"), "resolve open dispatch should sync export path to PDF suffix")
	testing.expect(t, strings.contains(export_path, tmp_dir), "resolve open dispatch should sync export path into project directory")
	testing.expect(t, last_autosave_at != 99, "resolve open dispatch should refresh autosave timestamp")
}

@test
gui_resolve_confirm_action_open_branch_missing_file_preserves_state :: proc(t: ^testing.T) {
	state := core.new_initial_state()
	state.story_idea = "still-here"
	controller := ui.new_controller(state)
	defer ui.dispose_controller(&controller)

	project_path := "./missing-confirm-open"
	export_path := "./out.pdf"
	is_dirty := true
	last_autosave_at: f64 = 55

	msg := gui.resolve_confirm_action_with_message(.Open_Project, &controller, &project_path, &export_path, .PDF, &is_dirty, &last_autosave_at)
	defer delete(project_path)
	testing.expect(t, strings.contains(msg, "project file does not exist"), "resolve open branch should surface missing-file error")
	testing.expect(t, controller.state.story_idea == "still-here", "resolve open failure should preserve existing state")
	testing.expect(t, is_dirty, "resolve open failure should preserve dirty flag")
	testing.expect(t, last_autosave_at == 55, "resolve open failure should preserve autosave timestamp")
}

@test
gui_autosave_tick_success_writes_project_and_clears_dirty :: proc(t: ^testing.T) {
	tmp_dir, terr := os.make_directory_temp("", "comic-gui-autosave-*", context.temp_allocator)
	testing.expect(t, terr == nil, "temp dir should be created")
	if terr != nil {
		return
	}
	defer os.remove_all(tmp_dir)

	project_path := fmt.aprintf("%s/autosave_target.comic.json", tmp_dir)
	defer delete(project_path)
	state := core.new_initial_state()
	is_dirty := true
	last_autosave_at: f64 = -1
	last_save_at: f64 = -1

	msg := gui.autosave_tick_with_message(&project_path, state, true, &is_dirty, &last_autosave_at, &last_save_at, 0)
	defer delete(msg)
	testing.expect(t, strings.has_prefix(msg, "Autosaved:"), "autosave tick should report success when project write works")
	testing.expect(t, strings.has_suffix(project_path, ".comic.json"), "autosave tick should normalize project path suffix")
	testing.expect(t, os.exists(project_path), "autosave tick should write project file")
	testing.expect(t, !is_dirty, "autosave tick should clear dirty flag after successful save")
	testing.expect(t, last_save_at >= 0, "autosave tick should set last-save timestamp on success")
}


@test
gui_write_diagnostics_with_message_creates_file :: proc(t: ^testing.T) {
	tmp_dir, terr := os.make_directory_temp("", "comic-gui-diag-*", context.temp_allocator)
	testing.expect(t, terr == nil, "temp dir should be created")
	if terr != nil {
		return
	}
	defer os.remove_all(tmp_dir)

	project_path := fmt.aprintf("%s/project.comic.json", tmp_dir)
	defer delete(project_path)
	export_path := fmt.aprintf("%s/out.pdf", tmp_dir)
	defer delete(export_path)

	controller := ui.new_controller(core.new_initial_state())
	defer ui.dispose_controller(&controller)
	action_log: gui.Action_Log
	defer gui.action_log_dispose(&action_log)
	ctx := gui.make_diagnostics_action_context(&controller, &action_log, false, true, true, true, 20, project_path, export_path, 6, false)

	msg := gui.write_diagnostics_with_message(ctx)
	defer delete(msg)
	testing.expect(t, strings.has_prefix(msg, "Wrote diagnostics file:"), "diagnostics write helper should report written file")

	diag_path := fmt.aprintf("%s/gui_diagnostics.txt", tmp_dir)
	defer delete(diag_path)
	testing.expect(t, os.exists(diag_path), "diagnostics write helper should create diagnostics file")
}

@test
gui_write_session_report_with_message_creates_file :: proc(t: ^testing.T) {
	tmp_dir, terr := os.make_directory_temp("", "comic-gui-report-*", context.temp_allocator)
	testing.expect(t, terr == nil, "temp dir should be created")
	if terr != nil {
		return
	}
	defer os.remove_all(tmp_dir)

	project_path := fmt.aprintf("%s/project.comic.json", tmp_dir)
	defer delete(project_path)
	export_path := fmt.aprintf("%s/out.cbz", tmp_dir)
	defer delete(export_path)

	controller := ui.new_controller(core.new_initial_state())
	defer ui.dispose_controller(&controller)
	action_log: gui.Action_Log
	defer gui.action_log_dispose(&action_log)
	status_msg := fmt.aprintf("")
	defer delete(status_msg)
	gui.push_status(&status_msg, &action_log, "seed log entry")

	ctx := gui.make_diagnostics_action_context(&controller, &action_log, true, true, true, true, 30, project_path, export_path, 4, true)
	msg := gui.write_session_report_with_message(ctx)
	defer delete(msg)
	testing.expect(t, strings.has_prefix(msg, "Wrote session report:"), "session report write helper should report written file")

	report_path := fmt.aprintf("%s/gui_session_report.txt", tmp_dir)
	defer delete(report_path)
	testing.expect(t, os.exists(report_path), "session report helper should create report file")
}

@test
gui_write_diagnostics_with_message_failure_for_missing_dir :: proc(t: ^testing.T) {
	controller := ui.new_controller(core.new_initial_state())
	defer ui.dispose_controller(&controller)
	action_log: gui.Action_Log
	defer gui.action_log_dispose(&action_log)

	project_path := "./missing-dir/sub/project.comic.json"
	export_path := "./out.pdf"
	ctx := gui.make_diagnostics_action_context(&controller, &action_log, false, true, true, true, 20, project_path, export_path, 6, false)

	msg := gui.write_diagnostics_with_message(ctx)
	defer delete(msg)
	testing.expect(t, msg == "Failed writing diagnostics file", "diagnostics write helper should report failure for missing directory")
}

@test
gui_write_session_report_with_message_failure_for_missing_dir :: proc(t: ^testing.T) {
	controller := ui.new_controller(core.new_initial_state())
	defer ui.dispose_controller(&controller)
	action_log: gui.Action_Log
	defer gui.action_log_dispose(&action_log)

	project_path := "./missing-dir/sub/project.comic.json"
	export_path := "./out.pdf"
	ctx := gui.make_diagnostics_action_context(&controller, &action_log, false, true, true, true, 20, project_path, export_path, 6, false)

	msg := gui.write_session_report_with_message(ctx)
	defer delete(msg)
	testing.expect(t, msg == "Failed writing session report", "session report write helper should report failure for missing directory")
}
