"""User-visible import and export workflows.""" from unittest.mock import Mock import pytest from textual.widgets import Input, RadioButton, Static from hosts.core.import_export import ( ExportFormat, ExportResult, ImportFormat, ImportResult, ) from hosts.core.models import HostEntry, HostsFile from hosts.tui.app import HostsManagerApp from hosts.tui.import_export_modal import ExportModal, FileBrowserModal, ImportModal @pytest.mark.asyncio async def test_export_shortcut_opens_a_format_chooser_and_reports_validation(): """Export presents every core format and keeps invalid input visible.""" app = HostsManagerApp() app.hosts_file = HostsFile( entries=[HostEntry(ip_address="192.0.2.10", hostnames=["export.test"])] ) app.load_hosts_file = Mock() async with app.run_test(size=(120, 40)) as pilot: await pilot.press("ctrl+x") await pilot.pause() assert isinstance(app.screen, ExportModal) assert [button.label.plain for button in app.screen.query(RadioButton)] == [ "Hosts file (.hosts)", "JSON (.json)", "CSV (.csv)", ] app.screen.query_one("#path-input", Input).value = "" await pilot.click("#continue-button") assert "Choose a file path" in str( app.screen.query_one("#workflow-error", Static).render() ) @pytest.mark.asyncio async def test_export_modal_is_centered_in_the_terminal_viewport(): """The export form is centered rather than pinned to a terminal corner.""" app = HostsManagerApp() app.load_hosts_file = Mock() async with app.run_test(size=(120, 40)) as pilot: await pilot.press("ctrl+x") await pilot.pause() container = app.screen.query_one("#format-container") assert container.region.x == (app.size.width - container.region.width) // 2 assert container.region.y == (app.size.height - container.region.height) // 2 @pytest.mark.asyncio async def test_export_browser_lets_the_user_choose_a_destination_directory(): """Browse opens a centered directory tree rather than requiring a typed path.""" app = HostsManagerApp() app.load_hosts_file = Mock() async with app.run_test(size=(120, 40)) as pilot: await pilot.press("ctrl+x") await pilot.pause() await pilot.press("tab") await pilot.press("enter") await pilot.pause() await pilot.pause() assert isinstance(app.screen, FileBrowserModal) assert app.screen.query_one("#file-browser-tree") is not None container = app.screen.query_one("#file-browser-container") assert container.region.x == (app.size.width - container.region.width) // 2 await pilot.press("tab") await pilot.press("tab") await pilot.press("enter") await pilot.pause() await pilot.pause() assert isinstance(app.screen, ExportModal) assert app.screen.query_one("#path-input", Input).value.endswith( "hosts-export.hosts" ) @pytest.mark.asyncio async def test_export_requires_overwrite_confirmation_before_calling_service(tmp_path): """An existing output file is not replaced until the user confirms it.""" target = tmp_path / "hosts.json" target.write_text("existing", encoding="utf-8") app = HostsManagerApp() app.hosts_file = HostsFile( entries=[HostEntry(ip_address="192.0.2.10", hostnames=["export.test"])] ) app.import_export_service = Mock() app.import_export_service.get_supported_export_formats.return_value = list( ExportFormat ) app.import_export_service.validate_export_path.return_value = [ f"File {target} already exists and will be overwritten" ] app.import_export_service.export_json_format.return_value = ExportResult( success=True, file_path=target, entries_exported=1, errors=[], format=ExportFormat.JSON, ) async with app.run_test(size=(120, 40)) as pilot: app.action_export_entries() await pilot.pause() app.screen.query_one("#path-input", Input).value = str(target) app.screen.query_one("#format-json", RadioButton).value = True await pilot.click("#continue-button") assert "already exists" in str( app.screen.query_one("#workflow-warning", Static).render() ) app.import_export_service.export_json_format.assert_not_called() await pilot.click("#confirm-button") await pilot.pause() await pilot.pause() app.import_export_service.export_json_format.assert_called_once_with( app.hosts_file, target ) @pytest.mark.asyncio async def test_import_shortcut_requires_privileged_mode_and_surfaces_service_errors(): """Import cannot mutate in Read-only Mode and reports parser failures.""" app = HostsManagerApp() app.hosts_file = HostsFile( entries=[HostEntry(ip_address="192.0.2.10", hostnames=["original.test"])] ) app.load_hosts_file = Mock() async with app.run_test(size=(120, 40)) as pilot: await pilot.press("ctrl+o") await pilot.pause() assert not isinstance(app.screen, ImportModal) assert "Read-only Mode" in str(app.query_one("#message-rail", Static).render()) app.edit_mode = True app.manager.edit_mode = True app.import_export_service = Mock() app.import_export_service.get_supported_import_formats.return_value = list( ImportFormat ) app.import_export_service.import_csv_format.return_value = Mock( success=False, errors=["CSV column 'ip_address' is missing"] ) app.action_import_entries() await pilot.pause() assert isinstance(app.screen, ImportModal) app.screen.query_one("#path-input", Input).value = "/tmp/broken.csv" csv_button = app.screen.query_one("#format-csv", RadioButton) csv_button.value = True await pilot.click("#import-button") await pilot.pause() assert "CSV column 'ip_address' is missing" in str( app.screen.query_one("#workflow-error", Static).render() ) @pytest.mark.asyncio async def test_successful_import_replaces_entries_and_saves_through_the_manager(): """A valid import reaches the persistence seam without a real Hosts File.""" app = HostsManagerApp() app.hosts_file = HostsFile( entries=[HostEntry(ip_address="192.0.2.10", hostnames=["original.test"])] ) app.edit_mode = True app.manager.edit_mode = True app.save_mutation = Mock(return_value=True) app.import_export_service = Mock() app.import_export_service.get_supported_import_formats.return_value = list( ImportFormat ) imported = HostEntry(ip_address="198.51.100.3", hostnames=["imported.test"]) app.import_export_service.import_json_format.return_value = ImportResult( success=True, entries=[imported], errors=[], warnings=[], total_processed=1, successfully_imported=1, ) async with app.run_test(size=(120, 40)) as pilot: app.action_import_entries() await pilot.pause() app.screen.query_one("#path-input", Input).value = "/tmp/entries.json" app.screen.query_one("#format-json", RadioButton).value = True await pilot.click("#import-button") await pilot.pause() await pilot.pause() assert [entry.hostnames for entry in app.hosts_file.entries] == [ ["imported.test"] ] app.save_mutation.assert_called_once() assert "Imported and saved 1 Host Entries" in str( app.query_one("#message-rail", Static).render() )