import subprocess
import unittest
from collections import OrderedDict
from pathlib import Path
from unittest.mock import Mock, patch

from pydantic import ValidationError

from jupyter_deploy.engine.engine_config import ReadConfigurationError
from jupyter_deploy.engine.supervised_execution import ExecutionError
from jupyter_deploy.engine.terraform.tf_config import TerraformConfigHandler
from jupyter_deploy.engine.terraform.tf_constants import (
    TF_DEFAULT_PLAN_FILENAME,
    TF_ENGINE_DIR,
    TF_PRESETS_DIR,
)
from jupyter_deploy.engine.terraform.tf_enums import TerraformSequenceId
from jupyter_deploy.engine.vardefs import (
    BoolTemplateVariableDefinition,
    DictStrTemplateVariableDefinition,
    FloatTemplateVariableDefinition,
    ListStrTemplateVariableDefinition,
    StrTemplateVariableDefinition,
    TemplateVariableDefinition,
)
from jupyter_deploy.handlers.command_history_handler import LogCleanupError


class TestTerraformConfigHandler(unittest.TestCase):
    MOCK_OVERRIDE_PRESET_PATH = Path("/mock/path/engine/jdinputs.preset.override.tfvars")
    MOCK_RECORD_VARS_PATH = Path("/mock/path/engine/jdinputs.auto.tfvars")
    MOCK_RECORD_SECRETS_PATH = Path("/mock/path/engine/jdinputs.secrets.auto.tfvars")

    def get_mock_command_history(self) -> Mock:
        """Return a mock CommandHistoryHandler."""
        mock_history = Mock()
        mock_history.create_log_file.return_value = Path("/mock/path/.jd-history/config/20260129-143022.log")
        mock_history.clear_logs.return_value = Mock()  # Returns cleanup result
        return mock_history

    def get_mock_variable_handler_and_fns(self) -> tuple[Mock, dict[str, Mock]]:
        mock_handler = Mock()
        mock_get_recorded_variables_filepath = Mock()
        mock_get_recorded_secrets_filepath = Mock()
        mock_reset_recorded_variables = Mock()
        mock_reset_recorded_secrets = Mock()
        mock_sync_engine_varfiles = Mock()
        mock_sync_variables_config = Mock()
        mock_get_template_variables = Mock()
        mock_update_variable_records = Mock()
        mock_create_filtered_preset_file = Mock()

        mock_handler.get_recorded_variables_filepath = mock_get_recorded_variables_filepath
        mock_handler.get_recorded_secrets_filepath = mock_get_recorded_secrets_filepath
        mock_handler.reset_recorded_variables = mock_reset_recorded_variables
        mock_handler.reset_recorded_secrets = mock_reset_recorded_secrets
        mock_handler.sync_engine_varfiles_with_project_variables_config = mock_sync_engine_varfiles
        mock_handler.sync_project_variables_config = mock_sync_variables_config
        mock_handler.get_template_variables = mock_get_template_variables
        mock_handler.update_variable_records = mock_update_variable_records
        mock_handler.create_filtered_preset_file = mock_create_filtered_preset_file

        mock_get_recorded_variables_filepath.return_value = TestTerraformConfigHandler.MOCK_RECORD_VARS_PATH
        mock_get_recorded_secrets_filepath.return_value = TestTerraformConfigHandler.MOCK_RECORD_SECRETS_PATH
        mock_create_filtered_preset_file.return_value = TestTerraformConfigHandler.MOCK_OVERRIDE_PRESET_PATH

        return mock_handler, {
            "get_recorded_variables_filepath": mock_get_recorded_variables_filepath,
            "get_recorded_secrets_filepath": mock_get_recorded_secrets_filepath,
            "reset_recorded_variables": mock_reset_recorded_variables,
            "reset_recorded_secrets": mock_reset_recorded_secrets,
            "sync_engine_varfiles_with_project_variables_config": mock_sync_engine_varfiles,
            "sync_project_variables_config": mock_sync_variables_config,
            "get_template_variables": mock_get_template_variables,
            "update_variables_record": mock_update_variable_records,
            "create_filtered_preset_file": mock_create_filtered_preset_file,
        }

    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_class_can_instantiate(self, mock_variable_handler_cls: Mock) -> None:
        # Arrange
        path = Path("/fake/path")
        manifest = Mock()
        mock_history = self.get_mock_command_history()

        mock_vars_handler, mock_vars_fns = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler
        handler = TerraformConfigHandler(path, manifest, mock_history)

        # Assert
        self.assertIsNotNone(handler)
        self.assertEqual(handler.plan_out_path, path / TF_ENGINE_DIR / TF_DEFAULT_PLAN_FILENAME)
        self.assertEqual(handler.project_manifest, manifest)
        self.assertEqual(handler.tf_variables_handler, mock_vars_handler)

        # expensive methods of EngineVariablesHandler are not called
        mock_vars_fns["get_template_variables"].assert_not_called()
        mock_vars_fns["update_variables_record"].assert_not_called()

    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_class_uses_custom_output_file_when_provided(self, mock_variable_handler_cls: Mock) -> None:
        # Arrange
        path = Path("/fake/path")
        manifest = Mock()
        custom_output = "custom-output-file"
        mock_vars_handler, _ = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler
        handler = TerraformConfigHandler(path, manifest, self.get_mock_command_history(), output_filename=custom_output)

        # Assert
        self.assertIsNotNone(handler)
        self.assertEqual(handler.plan_out_path, path / TF_ENGINE_DIR / custom_output)
        self.assertEqual(handler.tf_variables_handler, mock_vars_handler)

    @patch("jupyter_deploy.fs_utils.file_exists")
    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_verify_preset_exists_calls_fs_util(self, mock_variable_handler_cls: Mock, mock_file_exists: Mock) -> None:
        # Arrange
        path = Path("/fake/path")
        mock_vars_handler, _ = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler
        handler = TerraformConfigHandler(path, Mock(), command_history_handler=self.get_mock_command_history())

        # Act
        handler.verify_preset_exists("all")

        # Assert
        mock_file_exists.assert_called_once_with(
            file_path=path / TF_ENGINE_DIR / TF_PRESETS_DIR / "defaults-all.tfvars"
        )

    @patch("jupyter_deploy.fs_utils.find_matching_filenames")
    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_list_presets_calls_fs_util(self, mock_variable_handler_cls: Mock, mock_find: Mock) -> None:
        mock_find.return_value = [
            "defaults-all.tfvars",
            "defaults-base.tfvars",
            "defaults-all-except-instance.tfvars",
        ]

        # Arrange
        path = Path("/fake/path")
        mock_vars_handler, _ = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler
        handler = TerraformConfigHandler(path, Mock(), command_history_handler=self.get_mock_command_history())

        # Act
        presets = handler.list_presets()

        # Assert
        self.assertEqual(sorted(["all", "all-except-instance", "base", "none"]), sorted(presets))
        mock_find.assert_called_once_with(
            dir_path=handler.engine_dir_path / TF_PRESETS_DIR,
            file_pattern="defaults-*.tfvars",
        )

    @patch("jupyter_deploy.fs_utils.find_matching_filenames")
    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_list_presets_always_returns_none(self, mock_variable_handler_cls: Mock, mock_find: Mock) -> None:
        mock_find.return_value = []

        # Arrange
        path = Path("/fake/path")
        mock_vars_handler, _ = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler
        handler = TerraformConfigHandler(path, Mock(), command_history_handler=self.get_mock_command_history())

        # Act
        presets = handler.list_presets()

        # Assert
        self.assertEqual(["none"], presets)

    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_reset_recorded_variables_calls_vars_handler(self, mock_variable_handler_cls: Mock) -> None:
        # Arrange
        path = Path("/fake/path")
        mock_vars_handler, mock_vars_fns = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler
        handler = TerraformConfigHandler(path, Mock(), command_history_handler=self.get_mock_command_history())

        # Act
        handler.reset_recorded_variables()

        # Assert
        mock_vars_fns["reset_recorded_variables"].assert_called_once()

    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_reset_recorded_secrets_calls_vars_handler(self, mock_variable_handler_cls: Mock) -> None:
        # Arrange
        path = Path("/fake/path")
        mock_vars_handler, mock_vars_fns = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler
        handler = TerraformConfigHandler(path, Mock(), command_history_handler=self.get_mock_command_history())

        # Act
        handler.reset_recorded_secrets()

        # Assert
        mock_vars_fns["reset_recorded_secrets"].assert_called_once()

    @patch("jupyter_deploy.engine.terraform.tf_supervised_executor_factory.create_terraform_executor")
    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_configure_calls_tf_init(self, mock_variable_handler_cls: Mock, mock_create_executor: Mock) -> None:
        # Arrange
        mock_vars_handler, _ = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler

        # Mock executor for both init and plan
        mock_executor = Mock()
        mock_executor.execute.return_value = 0  # Return code 0 (success)
        mock_create_executor.return_value = mock_executor

        handler = TerraformConfigHandler(Path("/fake/path"), Mock(), self.get_mock_command_history())

        # Act
        handler.configure()

        # Assert - both init and plan executors should be created
        self.assertEqual(mock_create_executor.call_count, 2)
        init_call = mock_create_executor.mock_calls[0]
        self.assertEqual(init_call[2]["exec_dir"], Path("/fake/path/engine"))

        # Verify execute was called on the executor
        self.assertEqual(mock_executor.execute.call_count, 2)

    @patch("jupyter_deploy.engine.terraform.tf_supervised_executor_factory.create_terraform_executor")
    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_configure_calls_tf_plan_with_a_named_plan(
        self, mock_variable_handler_cls: Mock, mock_create_executor: Mock
    ) -> None:
        # Arrange
        mock_vars_handler, mock_vars_fns = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler

        # Mock executor for both init and plan - both succeed
        mock_executor = Mock()
        mock_executor.execute.return_value = 0  # Return code 0 (success)
        mock_create_executor.return_value = mock_executor

        handler = TerraformConfigHandler(Path("/fake/path"), Mock(), self.get_mock_command_history())

        # Act
        handler.configure()

        # Assert - configure completed successfully (no exception raised)
        self.assertEqual(mock_create_executor.call_count, 2)  # Init and plan
        self.assertEqual(mock_executor.execute.call_count, 2)
        mock_vars_fns["sync_engine_varfiles_with_project_variables_config"].assert_called_once()

    @patch("jupyter_deploy.engine.terraform.tf_supervised_executor_factory.create_terraform_executor")
    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_configure_calls_tf_plan_passes_preset(
        self, mock_variable_handler_cls: Mock, mock_create_executor: Mock
    ) -> None:
        # Arrange
        mock_vars_handler, mock_vars_fns = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler

        # Mock executor for both init and plan - both succeed
        mock_executor = Mock()
        mock_executor.execute.return_value = 0  # Return code 0 (success)
        mock_create_executor.return_value = mock_executor

        path = Path("/fake/path")
        handler = TerraformConfigHandler(path, Mock(), command_history_handler=self.get_mock_command_history())

        # Act
        handler.configure(preset_name="all")

        # Assert - configure completed successfully (no exception raised)
        self.assertEqual(mock_create_executor.call_count, 2)  # Init and plan

        # Verify preset file was created
        expect_called_path = path / TF_ENGINE_DIR / TF_PRESETS_DIR / "defaults-all.tfvars"
        mock_vars_fns["sync_engine_varfiles_with_project_variables_config"].assert_called_once()
        mock_vars_fns["create_filtered_preset_file"].assert_called_once_with(expect_called_path)

    @patch("jupyter_deploy.engine.terraform.tf_supervised_executor_factory.create_terraform_executor")
    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_configure_calls_tf_plan_with_variable_override(
        self, mock_variable_handler_cls: Mock, mock_create_executor: Mock
    ) -> None:
        # Arrange
        mock_vars_handler, mock_vars_fns = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler

        # Mock executor for both init and plan - both succeed
        mock_executor = Mock()
        mock_executor.execute.return_value = 0  # Return code 0 (success)
        mock_create_executor.return_value = mock_executor

        path = Path("/fake/path")
        handler = TerraformConfigHandler(path, Mock(), command_history_handler=self.get_mock_command_history())

        mock_var1 = Mock(spec=StrTemplateVariableDefinition)
        mock_var2 = Mock(spec=FloatTemplateVariableDefinition)
        mock_var3 = Mock(spec=BoolTemplateVariableDefinition)
        mock_var4 = Mock(spec=ListStrTemplateVariableDefinition)
        mock_var5 = Mock(spec=DictStrTemplateVariableDefinition)
        mock_variables: dict[str, TemplateVariableDefinition] = OrderedDict(
            {"var1": mock_var1, "var2": mock_var2, "var3": mock_var3, "var4": mock_var4, "var5": mock_var5}
        )
        for idx, key in enumerate(mock_variables.keys()):
            mock_variables[key].variable_name = f"var{idx + 1}"

        mock_var1.assigned_value = "some-value"
        mock_var2.assigned_value = 3.1459
        mock_var3.assigned_value = True
        mock_var4.assigned_value = ["email1@example.com", "email2@example.com"]
        mock_var5.assigned_value = {"Key1": "Val1", "Key2": "Val2"}

        # Act
        handler.configure(preset_name="all", variable_overrides=mock_variables)

        # Assert - configure completed successfully (no exception raised)
        self.assertEqual(mock_create_executor.call_count, 2)  # Init and plan
        mock_vars_fns["sync_engine_varfiles_with_project_variables_config"].assert_called_once()
        mock_vars_fns["create_filtered_preset_file"].assert_called_once()

        # Verify the executor was called with the plan commands
        plan_executor_call = mock_executor.execute.mock_calls[1]  # Second call is for plan
        plan_cmds = plan_executor_call[1][0]
        plan_cmds_len = len(plan_cmds)

        # Verify variable overrides were passed to terraform plan
        self.assertEqual("-var", plan_cmds[plan_cmds_len - 10])
        self.assertEqual("var1=some-value", plan_cmds[plan_cmds_len - 9])

        self.assertEqual("-var", plan_cmds[plan_cmds_len - 8])
        self.assertEqual("var2=3.1459", plan_cmds[plan_cmds_len - 7])

        self.assertEqual("-var", plan_cmds[plan_cmds_len - 6])
        self.assertEqual("var3=true", plan_cmds[plan_cmds_len - 5])

        self.assertEqual("-var", plan_cmds[plan_cmds_len - 4])
        self.assertEqual('var4=["email1@example.com", "email2@example.com"]', plan_cmds[plan_cmds_len - 3])

        self.assertEqual("-var", plan_cmds[plan_cmds_len - 2])
        self.assertEqual('var5={"Key1": "Val1", "Key2": "Val2"}', plan_cmds[plan_cmds_len - 1])

    @patch("jupyter_deploy.engine.terraform.tf_supervised_executor_factory.create_terraform_executor")
    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_configure_does_not_call_plan_if_tf_init_fails(
        self, mock_variable_handler_cls: Mock, mock_create_executor: Mock
    ) -> None:
        # Arrange
        mock_vars_handler, mock_vars_fns = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler

        # Mock executor for init - fails with non-zero return code
        mock_executor = Mock()
        mock_executor.execute.return_value = 1  # Return code 1 (failure)
        mock_create_executor.return_value = mock_executor

        handler = TerraformConfigHandler(Path("/fake/path"), Mock(), self.get_mock_command_history())

        # Act & Assert - should raise ExecutionError
        with self.assertRaises(ExecutionError) as context:
            handler.configure()

        self.assertEqual(context.exception.retcode, 1)
        self.assertEqual(context.exception.command, "config")
        self.assertEqual(mock_create_executor.call_count, 1)  # Only init should be called
        self.assertEqual(mock_executor.execute.call_count, 1)
        mock_vars_fns["sync_engine_varfiles_with_project_variables_config"].assert_not_called()

    @patch("jupyter_deploy.engine.terraform.tf_supervised_executor_factory.create_terraform_executor")
    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_configure_does_not_call_plan_if_tf_init_timesout(
        self, mock_variable_handler_cls: Mock, mock_create_executor: Mock
    ) -> None:
        # Arrange
        mock_vars_handler, mock_vars_fns = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler

        # Mock executor for init - simulate timeout by raising TimeoutError
        mock_executor = Mock()
        mock_executor.execute.side_effect = TimeoutError("Command timed out")
        mock_create_executor.return_value = mock_executor

        handler = TerraformConfigHandler(Path("/fake/path"), Mock(), self.get_mock_command_history())

        # Act & Assert - should raise TimeoutError
        with self.assertRaises(TimeoutError):
            handler.configure()

        self.assertEqual(mock_create_executor.call_count, 1)  # Only init should be called
        self.assertEqual(mock_executor.execute.call_count, 1)
        mock_vars_fns["sync_engine_varfiles_with_project_variables_config"].assert_not_called()

    @patch("jupyter_deploy.engine.terraform.tf_supervised_executor_factory.create_terraform_executor")
    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_configure_print_to_console_if_plan_fails(
        self, mock_variable_handler_cls: Mock, mock_create_executor: Mock
    ) -> None:
        # Arrange
        mock_vars_handler, mock_vars_fns = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler

        # Mock executor: init succeeds, plan fails
        mock_executor = Mock()
        mock_executor.execute.side_effect = [0, 1]  # init=0 (success), plan=1 (failure)
        mock_create_executor.return_value = mock_executor

        handler = TerraformConfigHandler(Path("/fake/path"), Mock(), self.get_mock_command_history())

        # Act & Assert - should raise ExecutionError
        with self.assertRaises(ExecutionError) as context:
            handler.configure()

        self.assertEqual(context.exception.retcode, 1)
        self.assertEqual(context.exception.command, "config")
        self.assertEqual(mock_create_executor.call_count, 2)  # Both init and plan
        self.assertEqual(mock_executor.execute.call_count, 2)
        mock_vars_fns["sync_engine_varfiles_with_project_variables_config"].assert_called_once()

    @patch("jupyter_deploy.engine.terraform.tf_supervised_executor_factory.create_terraform_executor")
    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_configure_print_to_console_if_plan_timesout(
        self, mock_variable_handler_cls: Mock, mock_create_executor: Mock
    ) -> None:
        # Arrange
        mock_vars_handler, mock_vars_fns = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler

        # Mock executor: init succeeds, plan times out
        mock_executor = Mock()
        mock_executor.execute.side_effect = [0, TimeoutError("Command timed out")]  # init=0 (success), plan=timeout
        mock_create_executor.return_value = mock_executor

        handler = TerraformConfigHandler(Path("/fake/path"), Mock(), self.get_mock_command_history())

        # Act & Assert - should raise TimeoutError
        with self.assertRaises(TimeoutError):
            handler.configure()

        self.assertEqual(mock_create_executor.call_count, 2)  # Both init and plan
        self.assertEqual(mock_executor.execute.call_count, 2)
        mock_vars_fns["sync_engine_varfiles_with_project_variables_config"].assert_called_once()

    @patch("jupyter_deploy.engine.terraform.tf_plan_metadata.save_plan_metadata")
    @patch("jupyter_deploy.engine.terraform.tf_plan.extract_resource_counts_from_plan")
    @patch("jupyter_deploy.engine.terraform.tf_plan.extract_variables_from_plan")
    @patch("jupyter_deploy.engine.terraform.tf_plan.extract_plan")
    @patch("jupyter_deploy.cmd_utils.run_cmd_and_capture_output")
    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_record_only_plan_metadata_when_both_flags_are_false(
        self,
        mock_variable_handler_cls: Mock,
        mock_capture: Mock,
        mock_parse: Mock,
        mock_extract_vars: Mock,
        mock_extract_counts: Mock,
        mock_save_metadata: Mock,
    ) -> None:
        # Arrange
        mock_vars_handler, mock_vars_fns = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler
        handler = TerraformConfigHandler(Path("/fake/path"), Mock(), self.get_mock_command_history())

        # Mock the plan parsing and resource counts extraction (always happens)
        mock_capture.return_value = "plan-json-string"
        mock_plan = Mock()
        mock_plan.resource_changes = []
        mock_parse.return_value = mock_plan
        mock_extract_counts.return_value = (5, 3, 2)  # to_add, to_change, to_destroy

        # Act
        handler.record()

        # Assert - plan is parsed and metadata is saved, but variables are not extracted or recorded
        mock_capture.assert_called_once()
        mock_parse.assert_called_once_with("plan-json-string")
        mock_extract_counts.assert_called_once_with(mock_plan)
        mock_save_metadata.assert_called_once()

        # Variables should NOT be extracted when flags are false
        mock_extract_vars.assert_not_called()
        mock_vars_fns["sync_project_variables_config"].assert_not_called()

    @patch("jupyter_deploy.engine.terraform.tf_plan_metadata.save_plan_metadata")
    @patch("jupyter_deploy.engine.terraform.tf_plan.extract_resource_counts_from_plan")
    @patch("jupyter_deploy.fs_utils.write_inline_file_content")
    @patch("jupyter_deploy.engine.terraform.tf_plan.format_plan_variables")
    @patch("jupyter_deploy.engine.terraform.tf_plan.extract_variables_from_plan")
    @patch("jupyter_deploy.engine.terraform.tf_plan.extract_plan")
    @patch("jupyter_deploy.cmd_utils.run_cmd_and_capture_output")
    @patch("rich.console.Console")
    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_record_calls_plan_and_save_vars_only_on_var_flag(
        self,
        mock_variable_handler_cls: Mock,
        mock_console: Mock,
        mock_capture: Mock,
        mock_parse: Mock,
        mock_extract: Mock,
        mock_format: Mock,
        mock_write: Mock,
        mock_extract_counts: Mock,
        mock_save_metadata: Mock,
    ) -> None:
        # Arrange
        mock_vars_handler, mock_vars_fns = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler
        mock_console_instance = Mock()
        mock_console.return_value = mock_console_instance
        mock_capture.return_value = "i-am-a-serialized-plan"
        mock_plan = Mock()
        mock_plan.resource_changes = []
        mock_parse.return_value = mock_plan
        mock_extract_counts.return_value = (5, 3, 2)

        mock_var1 = Mock()
        mock_var2 = Mock()
        mock_secret1 = Mock()
        mock_var1.value = 1
        mock_var2.value = "two"
        mock_secret1.value = "nuclear-codes"
        mock_extract.return_value = ({"var1": mock_var1, "var2": mock_var2}, {"secret1": mock_secret1})
        mock_format.return_value = ["var1 = 1\n", 'var2 = "two"\n']

        path = Path("/fake/path")
        handler = TerraformConfigHandler(path, Mock(), command_history_handler=self.get_mock_command_history())

        # Act
        handler.record(record_vars=True)

        # Assert
        mock_capture.assert_called_once()
        mock_plan_cmd_capture = mock_capture.mock_calls[0]
        self.assertEqual(mock_plan_cmd_capture[1][0][:3], ["terraform", "show", "-json"])

        mock_parse.assert_called_once_with("i-am-a-serialized-plan")
        mock_extract_counts.assert_called_once_with(mock_plan)
        mock_save_metadata.assert_called_once()
        mock_extract.assert_called_once_with(mock_plan)
        mock_format.assert_called_once_with({"var1": mock_var1, "var2": mock_var2})
        mock_write.assert_called_once()

        mock_write_call = mock_write.mock_calls[0]
        self.assertEqual(mock_write_call[1][0], TestTerraformConfigHandler.MOCK_RECORD_VARS_PATH)
        self.assertIn("var1 = 1\n", mock_write_call[1][1])
        self.assertIn('var2 = "two"\n', mock_write_call[1][1])

        mock_vars_fns["sync_project_variables_config"].assert_called_once_with({"var1": 1, "var2": "two"})

    @patch("jupyter_deploy.engine.terraform.tf_plan_metadata.save_plan_metadata")
    @patch("jupyter_deploy.engine.terraform.tf_plan.extract_resource_counts_from_plan")
    @patch("jupyter_deploy.fs_utils.write_inline_file_content")
    @patch("jupyter_deploy.engine.terraform.tf_plan.format_plan_variables")
    @patch("jupyter_deploy.engine.terraform.tf_plan.extract_variables_from_plan")
    @patch("jupyter_deploy.engine.terraform.tf_plan.extract_plan")
    @patch("jupyter_deploy.cmd_utils.run_cmd_and_capture_output")
    @patch("rich.console.Console")
    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_record_calls_plan_and_save_secrets_only_on_secret_flag(
        self,
        mock_variable_handler_cls: Mock,
        mock_console: Mock,
        mock_capture: Mock,
        mock_parse: Mock,
        mock_extract: Mock,
        mock_format: Mock,
        mock_write: Mock,
        mock_extract_counts: Mock,
        mock_save_metadata: Mock,
    ) -> None:
        # Arrange
        mock_vars_handler, mock_vars_fns = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler
        mock_console_instance = Mock()
        mock_console.return_value = mock_console_instance
        mock_capture.return_value = "i-am-a-serialized-plan"
        mock_plan = Mock()
        mock_plan.resource_changes = []
        mock_parse.return_value = mock_plan
        mock_extract_counts.return_value = (5, 3, 2)

        mock_var1 = Mock()
        mock_var2 = Mock()
        mock_secret = Mock()
        mock_var1.value = 1
        mock_var2.value = "two"
        mock_secret.value = "nuclear-codes"
        mock_extract.return_value = ({"var1": mock_var1, "var2": mock_var2}, {"secret1": mock_secret})
        mock_format.return_value = ['secret1 = "nuclear-codes"\n']

        path = Path("/fake/path")
        handler = TerraformConfigHandler(path, Mock(), command_history_handler=self.get_mock_command_history())

        # Act
        handler.record(record_secrets=True)

        # Assert
        mock_capture.assert_called_once()
        mock_parse.assert_called_once_with("i-am-a-serialized-plan")
        mock_extract_counts.assert_called_once_with(mock_plan)
        mock_save_metadata.assert_called_once()
        mock_extract.assert_called_once_with(mock_plan)
        mock_format.assert_called_once_with({"secret1": mock_secret})
        mock_write.assert_called_once()

        mock_write_call = mock_write.mock_calls[0]
        self.assertEqual(mock_write_call[1][0], TestTerraformConfigHandler.MOCK_RECORD_SECRETS_PATH)
        self.assertIn('secret1 = "nuclear-codes"\n', mock_write_call[1][1])

        mock_vars_fns["sync_project_variables_config"].assert_called_once_with({"secret1": "nuclear-codes"})

    @patch("jupyter_deploy.engine.terraform.tf_plan_metadata.save_plan_metadata")
    @patch("jupyter_deploy.engine.terraform.tf_plan.extract_resource_counts_from_plan")
    @patch("jupyter_deploy.fs_utils.write_inline_file_content")
    @patch("jupyter_deploy.engine.terraform.tf_plan.format_plan_variables")
    @patch("jupyter_deploy.engine.terraform.tf_plan.extract_variables_from_plan")
    @patch("jupyter_deploy.engine.terraform.tf_plan.extract_plan")
    @patch("jupyter_deploy.cmd_utils.run_cmd_and_capture_output")
    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_record_should_call_plan_only_once_when_both_flags_are_passed(
        self,
        mock_variable_handler_cls: Mock,
        mock_capture: Mock,
        mock_parse: Mock,
        mock_extract: Mock,
        mock_format: Mock,
        mock_write: Mock,
        mock_extract_counts: Mock,
        mock_save_metadata: Mock,
    ) -> None:
        # Arrange
        mock_vars_handler, mock_vars_fns = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler

        # Mock command capture and plan parsing
        mock_capture.return_value = "i-am-a-serialized-plan"
        mock_plan = Mock()
        mock_plan.resource_changes = []
        mock_parse.return_value = mock_plan
        mock_extract_counts.return_value = (5, 3, 2)
        mock_var1 = Mock()
        mock_var2 = Mock()
        mock_secret = Mock()
        mock_var1.value = 1
        mock_var2.value = "two"
        mock_secret.value = "nuclear-codes"
        mock_extract.return_value = ({"var1": mock_var1, "var2": mock_var2}, {"secret1": mock_secret})
        mock_format.side_effect = [["var1 = 1\n", 'var2 = "two"\n'], ['secret1 = "nuclear-codes"\n']]

        path = Path("/fake/path")
        handler = TerraformConfigHandler(path, Mock(), command_history_handler=self.get_mock_command_history())

        # Act
        handler.record(record_vars=True, record_secrets=True)

        # Assert
        mock_capture.assert_called_once()
        mock_parse.assert_called_once_with("i-am-a-serialized-plan")
        mock_extract_counts.assert_called_once_with(mock_plan)
        mock_save_metadata.assert_called_once()
        mock_extract.assert_called_once_with(mock_plan)
        self.assertEqual(mock_format.call_count, 2)
        self.assertEqual(mock_write.call_count, 2)

        mock_write_vars_call = mock_write.mock_calls[0]
        self.assertEqual(mock_write_vars_call[1][0], TestTerraformConfigHandler.MOCK_RECORD_VARS_PATH)
        self.assertIn("var1 = 1\n", mock_write_vars_call[1][1])
        self.assertIn('var2 = "two"\n', mock_write_vars_call[1][1])

        mock_write_secrets_call = mock_write.mock_calls[1]
        self.assertEqual(mock_write_secrets_call[1][0], TestTerraformConfigHandler.MOCK_RECORD_SECRETS_PATH)
        self.assertIn('secret1 = "nuclear-codes"\n', mock_write_secrets_call[1][1])

        mock_vars_fns["sync_project_variables_config"].assert_called_once_with(
            {"var1": 1, "var2": "two", "secret1": "nuclear-codes"}
        )

    @patch("jupyter_deploy.fs_utils.write_inline_file_content")
    @patch("jupyter_deploy.cmd_utils.run_cmd_and_capture_output")
    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_catches_plan_retrieve_errors_and_print(
        self,
        mock_variable_handler_cls: Mock,
        mock_capture: Mock,
        mock_write: Mock,
    ) -> None:
        # Arrange
        mock_vars_handler, mock_vars_fns = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler
        mock_capture.side_effect = subprocess.CalledProcessError(
            1, ["terraform", "show", "-json"], "something went wrong", None
        )
        handler = TerraformConfigHandler(Path("/fake/path"), Mock(), self.get_mock_command_history())

        # Act & Assert
        with self.assertRaises(ReadConfigurationError):
            handler.record(record_vars=True)

        mock_write.assert_not_called()
        mock_vars_fns["sync_project_variables_config"].assert_not_called()

    @patch("jupyter_deploy.engine.terraform.tf_plan_metadata.save_plan_metadata")
    @patch("jupyter_deploy.engine.terraform.tf_plan.extract_resource_counts_from_plan")
    @patch("jupyter_deploy.fs_utils.write_inline_file_content")
    @patch("jupyter_deploy.engine.terraform.tf_plan.extract_variables_from_plan")
    @patch("jupyter_deploy.engine.terraform.tf_plan.extract_plan")
    @patch("jupyter_deploy.cmd_utils.run_cmd_and_capture_output")
    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_catches_plan_validation_errors(
        self,
        mock_variable_handler_cls: Mock,
        mock_capture: Mock,
        mock_parse: Mock,
        mock_extract: Mock,
        mock_write: Mock,
        mock_extract_counts: Mock,
        mock_save_metadata: Mock,
    ) -> None:
        # Arrange
        mock_vars_handler, mock_vars_fns = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler
        mock_capture.return_value = '{"resource_changes": []}'  # Valid JSON for metadata extraction
        mock_plan = Mock()
        mock_parse.return_value = mock_plan
        mock_extract_counts.return_value = (5, 3, 2)
        mock_extract.side_effect = ValidationError("some error", [])
        handler = TerraformConfigHandler(Path("/fake/path"), Mock(), self.get_mock_command_history())

        # Act & Assert
        with self.assertRaises(ReadConfigurationError):
            handler.record(record_secrets=True)

        mock_capture.assert_called_once()
        mock_parse.assert_called_once()
        mock_extract_counts.assert_called_once()
        mock_save_metadata.assert_called_once()
        mock_extract.assert_called_once()
        mock_write.assert_not_called()
        mock_vars_fns["sync_project_variables_config"].assert_not_called()

    @patch("jupyter_deploy.fs_utils.write_inline_file_content")
    @patch("jupyter_deploy.engine.terraform.tf_plan.extract_plan")
    @patch("jupyter_deploy.cmd_utils.run_cmd_and_capture_output")
    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_catches_plan_json_parse_errors(
        self,
        mock_variable_handler_cls: Mock,
        mock_capture: Mock,
        mock_parse: Mock,
        mock_write: Mock,
    ) -> None:
        # Arrange
        mock_vars_handler, mock_vars_fns = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler
        mock_capture.return_value = "i-am-a-serialized-plan"
        mock_parse.side_effect = ValueError("Invalid JSON")
        handler = TerraformConfigHandler(Path("/fake/path"), Mock(), self.get_mock_command_history())

        # Act & Assert
        with self.assertRaises(ReadConfigurationError):
            handler.record(record_secrets=True)

        mock_capture.assert_called_once()
        mock_parse.assert_called_once()
        mock_write.assert_not_called()
        mock_vars_fns["sync_project_variables_config"].assert_not_called()

    @patch("jupyter_deploy.engine.terraform.tf_config.TerraformSupervisedExecutionCallback")
    @patch("jupyter_deploy.engine.terraform.tf_supervised_executor_factory.create_terraform_executor")
    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_configure_with_terminal_handler_uses_supervised_callback(
        self,
        mock_variable_handler_cls: Mock,
        mock_create_executor: Mock,
        mock_callback_cls: Mock,
    ) -> None:
        """Test that configure with terminal_handler uses TerraformSupervisedExecutionCallback."""
        # Arrange
        mock_vars_handler, _ = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler

        # Mock executor for both init and plan - both succeed
        mock_executor = Mock()
        mock_executor.execute.return_value = 0
        mock_create_executor.return_value = mock_executor

        # Mock callback
        mock_callback = Mock()
        mock_callback_cls.return_value = mock_callback

        # Create handler WITH terminal_handler
        mock_terminal_handler = Mock()
        handler = TerraformConfigHandler(
            Path("/fake/path"),
            Mock(),
            self.get_mock_command_history(),
            terminal_handler=mock_terminal_handler,  # type: ignore[arg-type]
        )

        # Act
        handler.configure()

        # Assert - configure completed successfully (no exception raised)

        # Verify TerraformSupervisedExecutionCallback was created twice (init and plan)
        self.assertEqual(mock_callback_cls.call_count, 2)

        # Verify init callback
        init_callback_call = mock_callback_cls.call_args_list[0]
        self.assertEqual(init_callback_call.kwargs["terminal_handler"], mock_terminal_handler)
        self.assertEqual(init_callback_call.kwargs["sequence_id"], TerraformSequenceId.config_init)

        # Verify plan callback
        plan_callback_call = mock_callback_cls.call_args_list[1]
        self.assertEqual(plan_callback_call.kwargs["terminal_handler"], mock_terminal_handler)
        self.assertEqual(plan_callback_call.kwargs["sequence_id"], TerraformSequenceId.config_plan)

        # Verify executor was created with the supervised callback
        self.assertEqual(mock_create_executor.call_count, 2)
        init_executor_call = mock_create_executor.call_args_list[0]
        self.assertEqual(init_executor_call.kwargs["execution_callback"], mock_callback)

        plan_executor_call = mock_create_executor.call_args_list[1]
        self.assertEqual(plan_executor_call.kwargs["execution_callback"], mock_callback)

    @patch("jupyter_deploy.engine.terraform.tf_config.TerraformNoopExecutionCallback")
    @patch("jupyter_deploy.engine.terraform.tf_supervised_executor_factory.create_terraform_executor")
    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_configure_without_terminal_handler_uses_noop_callback(
        self,
        mock_variable_handler_cls: Mock,
        mock_create_executor: Mock,
        mock_noop_callback_cls: Mock,
    ) -> None:
        """Test that configure without terminal_handler uses TerraformNoopExecutionCallback."""
        # Arrange
        mock_vars_handler, _ = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler

        # Mock executor for both init and plan - both succeed
        mock_executor = Mock()
        mock_executor.execute.return_value = 0
        mock_create_executor.return_value = mock_executor

        # Mock noop callback
        mock_noop = Mock()
        mock_noop_callback_cls.return_value = mock_noop

        # Create handler WITHOUT terminal_handler
        handler = TerraformConfigHandler(
            Path("/fake/path"),
            Mock(),
            self.get_mock_command_history(),
            terminal_handler=None,
        )

        # Act
        handler.configure()

        # Assert - configure completed successfully (no exception raised)

        # Verify TerraformNoopExecutionCallback was created twice (init and plan)
        self.assertEqual(mock_noop_callback_cls.call_count, 2)

        # Verify executor was created with the noop callback
        self.assertEqual(mock_create_executor.call_count, 2)
        init_executor_call = mock_create_executor.call_args_list[0]
        self.assertEqual(init_executor_call.kwargs["execution_callback"], mock_noop)

        plan_executor_call = mock_create_executor.call_args_list[1]
        self.assertEqual(plan_executor_call.kwargs["execution_callback"], mock_noop)

    @patch("jupyter_deploy.engine.terraform.tf_supervised_executor_factory.create_terraform_executor")
    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_configure_clears_old_logs_on_success(
        self, mock_variable_handler_cls: Mock, mock_create_executor: Mock
    ) -> None:
        """Test that configure calls clear_logs on successful execution."""
        # Arrange
        mock_vars_handler, _ = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler

        # Mock executor for both init and plan - both succeed
        mock_executor = Mock()
        mock_executor.execute.return_value = 0
        mock_create_executor.return_value = mock_executor

        mock_history = self.get_mock_command_history()
        handler = TerraformConfigHandler(Path("/fake/path"), Mock(), mock_history)

        # Act
        handler.configure()

        # Assert - clear_logs should be called after successful execution
        mock_history.clear_logs.assert_called_once_with("config")

    @patch("jupyter_deploy.engine.terraform.tf_supervised_executor_factory.create_terraform_executor")
    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_configure_does_not_clear_logs_on_failure(
        self, mock_variable_handler_cls: Mock, mock_create_executor: Mock
    ) -> None:
        """Test that configure does NOT call clear_logs when plan fails."""
        # Arrange
        mock_vars_handler, _ = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler

        # Mock executor: init succeeds, plan fails
        mock_executor = Mock()
        mock_executor.execute.side_effect = [0, 1]
        mock_create_executor.return_value = mock_executor

        mock_history = self.get_mock_command_history()
        handler = TerraformConfigHandler(Path("/fake/path"), Mock(), mock_history)

        # Act & Assert - should raise ExecutionError
        with self.assertRaises(ExecutionError):
            handler.configure()

        # Assert - clear_logs should NOT be called on failure
        mock_history.clear_logs.assert_not_called()

    @patch("jupyter_deploy.engine.terraform.tf_supervised_executor_factory.create_terraform_executor")
    @patch("jupyter_deploy.engine.terraform.tf_variables.TerraformVariablesHandler")
    def test_configure_bubbles_up_clear_logs_exception(
        self, mock_variable_handler_cls: Mock, mock_create_executor: Mock
    ) -> None:
        """Test that configure bubbles up LogCleanupError from clear_logs."""
        # Arrange
        mock_vars_handler, _ = self.get_mock_variable_handler_and_fns()
        mock_variable_handler_cls.return_value = mock_vars_handler

        # Mock executor for both init and plan - both succeed
        mock_executor = Mock()
        mock_executor.execute.return_value = 0
        mock_create_executor.return_value = mock_executor

        mock_history = self.get_mock_command_history()
        mock_history.clear_logs.side_effect = LogCleanupError("Failed to delete 2 log file(s)")
        handler = TerraformConfigHandler(Path("/fake/path"), Mock(), mock_history)

        # Act & Assert - should raise LogCleanupError from clear_logs
        with self.assertRaises(LogCleanupError) as context:
            handler.configure()

        self.assertEqual(str(context.exception), "Failed to delete 2 log file(s)")
        mock_history.clear_logs.assert_called_once_with("config")
