from unittest.mock import MagicMock, patch

import numpy as np
import pytest
from qiskit.quantum_info import SparsePauliOp
from qiskit_aer.backends.aer_simulator import AerBackend

from quantum_pipeline.circuits import HFData
from quantum_pipeline.configs.module.backend import BackendConfig
from quantum_pipeline.solvers.vqe_solver import MaxFunctionEvalsReachedError, VQESolver
from quantum_pipeline.structures.vqe_observation import VQEResult


@pytest.fixture
def mock_backend_config():
    """Create a mock BackendConfig for testing."""
    return BackendConfig(
        local=True,
        gpu=False,
        optimization_level=2,
        min_num_qubits=4,
        filters=None,
        simulation_method='mock_backend',
        gpu_opts=None,
        noise=None,
    )


@pytest.fixture
def sample_hamiltonian():
    """Create a sample Hamiltonian for testing."""
    return SparsePauliOp.from_list([('IIII', 0.1), ('IIXI', 0.2), ('XIXI', 0.3)])


@pytest.fixture
def vqe_solver(mock_backend_config, sample_hamiltonian):
    """Create a VQESolver instance for testing."""
    return VQESolver(
        qubit_op=sample_hamiltonian,
        backend_config=mock_backend_config,
        max_iterations=10,
        optimizer='COBYLA',
        ansatz_reps=2,
    )


def test_vqe_solver_initialization(vqe_solver, sample_hamiltonian, mock_backend_config):
    """Test VQESolver initialization."""
    assert vqe_solver.qubit_op == sample_hamiltonian
    assert vqe_solver.backend_config == mock_backend_config
    assert vqe_solver.max_iterations == 10
    assert vqe_solver.optimizer == 'COBYLA'
    assert vqe_solver.ansatz_reps == 2
    assert vqe_solver.current_iter == 1
    assert len(vqe_solver.vqe_process) == 0


def test_compute_energy(vqe_solver):
    """Test compute_energy method."""
    # mocking the estimator
    mock_estimator = MagicMock()
    mock_result = MagicMock()
    mock_result.data.evs = [1.5]
    mock_result.data.stds = [0.1]
    mock_estimator.run.return_value.result.return_value = [mock_result]

    # mock the ansatz and hamiltonian
    mock_ansatz = MagicMock()
    mock_hamiltonian = MagicMock()

    # test params
    params = np.array([0.1, 0.2, 0.3])

    # suppress logging output
    with patch.object(vqe_solver.logger, 'debug'):
        energy = vqe_solver.compute_energy(params, mock_ansatz, mock_hamiltonian, mock_estimator)

    assert energy == 1.5
    assert len(vqe_solver.vqe_process) == 1
    assert vqe_solver.vqe_process[0].iteration == 1
    assert vqe_solver.vqe_process[0].result == 1.5
    assert vqe_solver.current_iter == 2


def test_compute_energy_derived_features(vqe_solver):
    """Test that compute_energy populates derived ML features across iterations."""
    mock_ansatz = MagicMock()
    mock_hamiltonian = MagicMock()

    # Helper to create a mock estimator returning given energy/std
    def make_estimator(energy, std):
        mock_estimator = MagicMock()
        mock_result = MagicMock()
        mock_result.data.evs = [energy]
        mock_result.data.stds = [std]
        mock_estimator.run.return_value.result.return_value = [mock_result]
        return mock_estimator

    params1 = np.array([1.0, 2.0, 3.0])
    params2 = np.array([1.1, 2.2, 3.3])
    params3 = np.array([1.0, 2.0, 3.0])

    with patch.object(vqe_solver.logger, 'debug'):
        # Iteration 1: first iteration — deltas are None
        vqe_solver.compute_energy(params1, mock_ansatz, mock_hamiltonian, make_estimator(-1.0, 0.1))
        p1 = vqe_solver.vqe_process[0]
        assert p1.energy_delta is None
        assert p1.parameter_delta_norm is None
        assert p1.cumulative_min_energy == np.float64(-1.0)

        # Iteration 2: energy improves
        vqe_solver.compute_energy(params2, mock_ansatz, mock_hamiltonian, make_estimator(-1.5, 0.05))
        p2 = vqe_solver.vqe_process[1]
        assert p2.energy_delta == pytest.approx(-0.5)
        expected_norm = np.linalg.norm(params2 - params1)
        assert p2.parameter_delta_norm == pytest.approx(expected_norm)
        assert p2.cumulative_min_energy == np.float64(-1.5)

        # Iteration 3: energy worsens — cumulative_min stays at -1.5
        vqe_solver.compute_energy(params3, mock_ansatz, mock_hamiltonian, make_estimator(-0.8, 0.2))
        p3 = vqe_solver.vqe_process[2]
        assert p3.energy_delta == pytest.approx(0.7)
        expected_norm3 = np.linalg.norm(params3 - params2)
        assert p3.parameter_delta_norm == pytest.approx(expected_norm3)
        assert p3.cumulative_min_energy == np.float64(-1.5)


def test_optimize_circuits(vqe_solver):
    """Test _optimize_circuits method."""
    # mock backend and its target
    mock_backend = MagicMock()
    mock_backend.target = MagicMock()

    # mock ansatz and hamiltonian
    mock_ansatz = MagicMock()
    mock_hamiltonian = MagicMock()

    # mock circuit optimization via pass_manager
    with patch('quantum_pipeline.solvers.vqe_solver.generate_preset_pass_manager') as mock_pm:
        # mock the pm instance
        mock_pass_manager = MagicMock()
        mock_pass_manager.run.return_value = mock_ansatz
        mock_pm.return_value = mock_pass_manager

        # mock the apply layout method
        mock_hamiltonian.apply_layout.return_value = mock_hamiltonian

        # call the method
        optimized_ansatz, _optimized_hamiltonian = vqe_solver._optimize_circuits(
            mock_ansatz, mock_hamiltonian, mock_backend
        )

        mock_pm.assert_called_once_with(
            target=mock_backend.target, optimization_level=vqe_solver.optimization_level
        )
        assert optimized_ansatz == mock_ansatz
        mock_hamiltonian.apply_layout.assert_called_once()


def test_solve_via_aer(vqe_solver):
    """Test solve method with Aer backend."""
    mock_backend = MagicMock(spec=AerBackend)

    # patch the backend
    with (
        patch.object(vqe_solver, 'get_backend', return_value=mock_backend),
        patch.object(vqe_solver, 'via_aer') as mock_via_aer,
    ):
        # assign mock result
        mock_result = MagicMock()
        mock_via_aer.return_value = mock_result

        # call the solver
        result = vqe_solver.solve()

        vqe_solver.get_backend.assert_called_once()
        mock_via_aer.assert_called_once_with(mock_backend)
        assert result == mock_result


def test_solve_via_ibmq(vqe_solver):
    """Test solve method with IBMQ backend."""
    mock_backend = MagicMock()

    # patch the get_backend to run via ibm quantum
    with (
        patch.object(vqe_solver, 'get_backend', return_value=mock_backend),
        patch.object(vqe_solver, 'via_ibmq') as mock_via_ibmq,
    ):
        # mock result
        mock_result = MagicMock()
        mock_via_ibmq.return_value = mock_result

        # call the solver
        result = vqe_solver.solve()

        vqe_solver.get_backend.assert_called_once()
        mock_via_ibmq.assert_called_once_with(mock_backend)
        assert result == mock_result


def test_vqe_solver_custom_parameters(mock_backend_config, sample_hamiltonian):
    """Test VQESolver with custom parameters."""
    custom_solver = VQESolver(
        qubit_op=sample_hamiltonian,
        backend_config=mock_backend_config,
        max_iterations=100,
        optimizer='SPSA',
        ansatz_reps=5,
        default_shots=2048,
        convergence_threshold=1e-4,
        optimization_level=2,
    )

    assert custom_solver.max_iterations == 100
    assert custom_solver.optimizer == 'SPSA'
    assert custom_solver.ansatz_reps == 5
    assert custom_solver.default_shots == 2048
    assert custom_solver.convergence_threshold == 1e-4
    assert custom_solver.optimization_level == 2


@pytest.mark.parametrize('backend_type', [AerBackend, MagicMock])
def test_solve_method_result_type(vqe_solver, backend_type):
    """Test that solve method returns a VQEResult for different backend types."""
    # mock backend
    mock_backend = MagicMock(spec=backend_type)
    mock_backend.name = 'test_backend'

    # mock result of the solver
    mock_result = MagicMock(spec=VQEResult)

    # patch get_backend to return the mock backend
    with patch.object(vqe_solver, 'get_backend', return_value=mock_backend):
        # patch method based on backend type
        if backend_type == AerBackend:
            with patch.object(vqe_solver, 'via_aer', return_value=mock_result):
                result = vqe_solver.solve()
        else:
            with patch.object(vqe_solver, 'via_ibmq', return_value=mock_result):
                result = vqe_solver.solve()

        # verify if correct result was returned
        assert isinstance(result, VQEResult | MagicMock)
        assert result._spec_class == VQEResult


class TestVQEConvergencePriority:
    """Test suite for VQE convergence and max_iterations priority logic."""

    def test_max_iterations_priority_over_convergence(self):
        """Test that max_iterations takes priority when both are specified."""
        # Test the logic directly without full VQE execution
        max_iterations = 5
        convergence_threshold = 1e-6

        # Logic from VQE solver: tol should be None when both are specified
        # tol = convergence_threshold if convergence_threshold and not max_iterations else None
        tol = convergence_threshold if convergence_threshold and not max_iterations else None

        assert tol is None, (
            'tol should be None when max_iterations takes priority over convergence'
        )

    def test_convergence_only_uses_tolerance(self):
        """Test that convergence threshold is used when only convergence is specified."""
        max_iterations = None
        convergence_threshold = 1e-6

        # Logic from VQE solver
        tol = convergence_threshold if convergence_threshold and not max_iterations else None

        assert tol == 1e-6, 'tol should be set when only convergence is specified'

    def test_max_iterations_only_no_tolerance(self):
        """Test that no tolerance is used when only max_iterations is specified."""
        max_iterations = 10
        convergence_threshold = None

        # Logic from VQE solver
        tol = convergence_threshold if convergence_threshold and not max_iterations else None

        assert tol is None, 'tol should be None when only max_iterations is specified'

    def test_neither_specified_no_tolerance(self):
        """Test that no tolerance is used when neither is specified."""
        max_iterations = None
        convergence_threshold = None

        # Logic from VQE solver
        tol = convergence_threshold if convergence_threshold and not max_iterations else None

        assert tol is None, 'tol should be None when neither is specified'

    def test_vqe_solver_initialization_with_both(self, sample_hamiltonian, mock_backend_config):
        """Test that VQESolver can be initialized with both parameters."""
        solver = VQESolver(
            qubit_op=sample_hamiltonian,
            backend_config=mock_backend_config,
            max_iterations=5,
            convergence_threshold=1e-6,
            optimizer='COBYLA',
            ansatz_reps=2,
        )

        assert solver.max_iterations == 5
        assert solver.convergence_threshold == 1e-6
        assert solver.optimizer == 'COBYLA'

    def test_vqe_solver_initialization_convergence_only(
        self, sample_hamiltonian, mock_backend_config
    ):
        """Test that VQESolver can be initialized with only convergence threshold."""
        solver = VQESolver(
            qubit_op=sample_hamiltonian,
            backend_config=mock_backend_config,
            max_iterations=None,
            convergence_threshold=1e-6,
            optimizer='COBYLA',
            ansatz_reps=2,
        )

        assert solver.max_iterations is None
        assert solver.convergence_threshold == 1e-6

    def test_vqe_solver_initialization_max_iterations_only(
        self, sample_hamiltonian, mock_backend_config
    ):
        """Test that VQESolver can be initialized with only max_iterations."""
        solver = VQESolver(
            qubit_op=sample_hamiltonian,
            backend_config=mock_backend_config,
            max_iterations=10,
            convergence_threshold=None,
            optimizer='COBYLA',
            ansatz_reps=2,
        )

        assert solver.max_iterations == 10
        assert solver.convergence_threshold is None

    def test_lbfgs_b_priority_logic(self):
        """Test L-BFGS-B specific logic when max_iterations is specified."""
        optimizer = 'L-BFGS-B'
        max_iterations = 5
        _convergence_threshold = 1e-6

        # Test the specific L-BFGS-B logic - should disable early convergence when max_iterations is set
        should_disable_early_convergence = optimizer == 'L-BFGS-B' and max_iterations

        assert should_disable_early_convergence, (
            'L-BFGS-B should disable default convergence when max_iterations is set'
        )

        # Verify the ftol and gtol values that would be set
        expected_ftol = 1e-15
        expected_gtol = 1e-15

        assert expected_ftol < 1e-10, 'ftol should be very small to prevent early convergence'
        assert expected_gtol < 1e-10, 'gtol should be very small to prevent early convergence'

    def test_lbfgs_b_without_max_iterations(self):
        """Test L-BFGS-B behavior when max_iterations is not specified."""
        optimizer = 'L-BFGS-B'
        max_iterations = None

        # Should NOT disable early convergence when max_iterations is not set
        should_disable_early_convergence = optimizer == 'L-BFGS-B' and max_iterations

        assert not should_disable_early_convergence, (
            'L-BFGS-B should use default convergence when max_iterations is not set'
        )


class TestVQEEdgeCases:
    """Test edge cases and boundary conditions for VQE solver."""

    def test_max_iterations_zero(self, sample_hamiltonian, mock_backend_config):
        """Test VQESolver behavior with max_iterations=0."""
        solver = VQESolver(
            qubit_op=sample_hamiltonian,
            backend_config=mock_backend_config,
            max_iterations=0,
            optimizer='COBYLA',
            ansatz_reps=2,
        )
        assert solver.max_iterations == 0

    def test_max_iterations_negative(self, sample_hamiltonian, mock_backend_config):
        """Test VQESolver behavior with negative max_iterations."""
        solver = VQESolver(
            qubit_op=sample_hamiltonian,
            backend_config=mock_backend_config,
            max_iterations=-5,
            optimizer='COBYLA',
            ansatz_reps=2,
        )
        assert solver.max_iterations == -5

    def test_max_iterations_very_large(self, sample_hamiltonian, mock_backend_config):
        """Test VQESolver behavior with very large max_iterations."""
        large_value = 999999
        solver = VQESolver(
            qubit_op=sample_hamiltonian,
            backend_config=mock_backend_config,
            max_iterations=large_value,
            optimizer='L-BFGS-B',
            ansatz_reps=2,
        )
        assert solver.max_iterations == large_value

    def test_convergence_threshold_zero(self, sample_hamiltonian, mock_backend_config):
        """Test VQESolver behavior with convergence_threshold=0."""
        solver = VQESolver(
            qubit_op=sample_hamiltonian,
            backend_config=mock_backend_config,
            max_iterations=10,
            convergence_threshold=0.0,
            optimizer='L-BFGS-B',
            ansatz_reps=2,
        )
        assert solver.convergence_threshold == 0.0

    def test_convergence_threshold_negative(self, sample_hamiltonian, mock_backend_config):
        """Test VQESolver behavior with negative convergence_threshold."""
        solver = VQESolver(
            qubit_op=sample_hamiltonian,
            backend_config=mock_backend_config,
            max_iterations=10,
            convergence_threshold=-1e-6,
            optimizer='L-BFGS-B',
            ansatz_reps=2,
        )
        assert solver.convergence_threshold == -1e-6

    def test_convergence_threshold_very_small(self, sample_hamiltonian, mock_backend_config):
        """Test VQESolver behavior with very small convergence_threshold."""
        tiny_threshold = 1e-20
        solver = VQESolver(
            qubit_op=sample_hamiltonian,
            backend_config=mock_backend_config,
            max_iterations=10,
            convergence_threshold=tiny_threshold,
            optimizer='L-BFGS-B',
            ansatz_reps=2,
        )
        assert solver.convergence_threshold == tiny_threshold

    def test_lbfgs_b_with_max_iterations_one(self, sample_hamiltonian, mock_backend_config):
        """Test L-BFGS-B behavior with max_iterations=1."""
        solver = VQESolver(
            qubit_op=sample_hamiltonian,
            backend_config=mock_backend_config,
            max_iterations=1,
            optimizer='L-BFGS-B',
            ansatz_reps=2,
        )

        # Should still disable default convergence criteria
        should_disable = solver.optimizer == 'L-BFGS-B' and solver.max_iterations
        assert should_disable

    def test_cobyla_with_max_iterations_one(self, sample_hamiltonian, mock_backend_config):
        """Test COBYLA behavior with max_iterations=1."""
        solver = VQESolver(
            qubit_op=sample_hamiltonian,
            backend_config=mock_backend_config,
            max_iterations=1,
            optimizer='COBYLA',
            ansatz_reps=2,
        )

        # COBYLA should not have special convergence handling
        should_disable = solver.optimizer == 'L-BFGS-B' and solver.max_iterations
        assert not should_disable

    def test_different_optimizers_with_max_iterations(
        self, sample_hamiltonian, mock_backend_config
    ):
        """Test that only L-BFGS-B gets special convergence handling."""
        optimizers = ['COBYLA', 'L-BFGS-B', 'COBYQA']

        for optimizer in optimizers:
            solver = VQESolver(
                qubit_op=sample_hamiltonian,
                backend_config=mock_backend_config,
                max_iterations=5,
                optimizer=optimizer,
                ansatz_reps=2,
            )

            should_disable = solver.optimizer == 'L-BFGS-B' and solver.max_iterations

            if optimizer == 'L-BFGS-B':
                assert should_disable, 'L-BFGS-B should disable convergence criteria'
            else:
                assert not should_disable, f'{optimizer} should not disable convergence criteria'

    def test_string_to_bool_edge_cases(self):
        """Test edge cases for convergence threshold truthiness."""
        # Test various falsy values for convergence_threshold
        falsy_values = [None, 0, 0.0, False]
        truthy_values = [1e-6, 1e-20, -1e-6, 1, True]

        for value in falsy_values:
            assert not bool(value), f'Value {value} should be falsy'

        for value in truthy_values:
            assert bool(value), f'Value {value} should be truthy'

    def test_optimization_params_structure(self):
        """Test that optimization parameters have correct structure."""
        max_iterations = 42

        # Base params that should always be present
        optimization_params = {
            'maxiter': max_iterations,
            'disp': False,
        }

        # L-BFGS-B specific additions
        if True:  # Simulating L-BFGS-B condition
            optimization_params.update(
                {
                    'ftol': 1e-15,
                    'gtol': 1e-15,
                }
            )

        assert 'maxiter' in optimization_params
        assert 'disp' in optimization_params
        assert optimization_params['maxiter'] == max_iterations
        assert optimization_params['disp'] is False
        assert optimization_params['ftol'] == 1e-15
        assert optimization_params['gtol'] == 1e-15


class TestMaxFunctionEvalsReachedError:
    """Tests for the hard function evaluation limit."""

    def test_exception_stores_fields(self):
        """Test that the exception stores iteration, best_params, and best_energy."""
        params = np.array([1.0, 2.0, 3.0])
        exc = MaxFunctionEvalsReachedError(iteration=50, best_params=params, best_energy=-1.117)
        assert exc.iteration == 50
        np.testing.assert_array_equal(exc.best_params, params)
        assert exc.best_energy == -1.117
        assert '50' in str(exc)

    def test_compute_energy_raises_at_limit(self, vqe_solver):
        """Test that compute_energy raises MaxFunctionEvalsReachedError when limit is exceeded."""
        # Simulate having already done max_iterations evaluations
        vqe_solver.max_iterations = 3
        vqe_solver.current_iter = 4  # one past the limit

        # Populate vqe_process with mock data so min() works
        mock_process = MagicMock()
        mock_process.cumulative_min_energy = -1.5
        mock_process.parameters = np.array([0.1, 0.2])
        vqe_solver.vqe_process = [mock_process]

        with pytest.raises(MaxFunctionEvalsReachedError) as exc_info:
            vqe_solver.compute_energy(
                np.array([0.1, 0.2]), MagicMock(), MagicMock(), MagicMock()
            )

        assert exc_info.value.iteration == 3
        assert exc_info.value.best_energy == -1.5

    def test_compute_energy_runs_within_limit(self, vqe_solver):
        """Test that compute_energy runs normally when within the limit."""
        vqe_solver.max_iterations = 5
        vqe_solver.current_iter = 1

        mock_estimator = MagicMock()
        mock_result = MagicMock()
        mock_result.data.evs = [-0.5]
        mock_result.data.stds = [0.01]
        mock_estimator.run.return_value.result.return_value = [mock_result]

        with patch.object(vqe_solver.logger, 'debug'):
            energy = vqe_solver.compute_energy(
                np.array([0.1, 0.2, 0.3]), MagicMock(), MagicMock(), mock_estimator
            )

        assert energy == -0.5
        assert vqe_solver.current_iter == 2

    def test_truncated_result_uses_best_energy(self, vqe_solver):
        """Test that _make_truncated_result uses the best observed energy."""
        vqe_solver.init_data = MagicMock()
        vqe_solver.vqe_process = [MagicMock(), MagicMock()]

        result = vqe_solver._make_truncated_result(
            best_energy=-1.5, best_params=np.array([1.0, 2.0]), elapsed=10.0
        )

        assert isinstance(result, VQEResult)
        assert result.minimum == np.float64(-1.5)
        assert result.minimization_time == np.float64(10.0)
        assert result.maxcv is None

    def test_no_limit_when_max_iterations_none(self, mock_backend_config, sample_hamiltonian):
        """Test that no hard limit is enforced when max_iterations is None."""
        solver = VQESolver(
            qubit_op=sample_hamiltonian,
            backend_config=mock_backend_config,
            max_iterations=None,
            optimizer='COBYLA',
            ansatz_reps=2,
        )
        solver.current_iter = 9999

        mock_estimator = MagicMock()
        mock_result = MagicMock()
        mock_result.data.evs = [-1.0]
        mock_result.data.stds = [0.01]
        mock_estimator.run.return_value.result.return_value = [mock_result]

        with patch.object(solver.logger, 'debug'):
            energy = solver.compute_energy(
                np.array([0.1, 0.2, 0.3]), MagicMock(), MagicMock(), mock_estimator
            )

        assert energy == -1.0


class TestVQESolverSeed:
    """Tests for seed-based reproducibility via VQESolver."""

    def test_seed_stored(self, mock_backend_config, sample_hamiltonian):
        """Test that seed is stored on the solver."""
        solver = VQESolver(
            qubit_op=sample_hamiltonian,
            backend_config=mock_backend_config,
            seed=42,
        )
        assert solver.seed == 42

    def test_seed_default_none(self, vqe_solver):
        """Test that seed defaults to None."""
        assert vqe_solver.seed is None

    @staticmethod
    def _run_via_aer_and_get_init_params(sample_hamiltonian, mock_backend_config, seed):
        """Helper: run via_aer with mocked optimizer/estimator, return initial_parameters."""
        solver = VQESolver(
            qubit_op=sample_hamiltonian,
            backend_config=mock_backend_config,
            max_iterations=1,
            optimizer='COBYLA',
            seed=seed,
        )

        mock_backend = MagicMock(spec=AerBackend)
        mock_backend.name = 'aer_simulator'
        mock_backend.target = MagicMock()

        mock_ansatz_isa = MagicMock()
        mock_ansatz_isa.num_parameters = 16
        mock_ansatz_isa.layout = None

        mock_hamiltonian_isa = MagicMock()
        mock_hamiltonian_isa.num_qubits = sample_hamiltonian.num_qubits
        mock_hamiltonian_isa.to_list.return_value = []

        mock_minimize_result = MagicMock()
        mock_minimize_result.fun = -1.0
        mock_minimize_result.x = np.zeros(16)
        mock_minimize_result.success = True

        with (
            patch.object(
                solver, '_optimize_circuits', return_value=(mock_ansatz_isa, mock_hamiltonian_isa)
            ),
            patch('quantum_pipeline.solvers.vqe_solver.EstimatorV2'),
            patch(
                'quantum_pipeline.solvers.vqe_solver.minimize', return_value=mock_minimize_result
            ),
        ):
            solver.via_aer(mock_backend)

        return solver.init_data.initial_parameters

    def test_same_seed_produces_identical_params_via_aer(
        self, mock_backend_config, sample_hamiltonian
    ):
        """Test that VQESolver.via_aer produces identical init params with the same seed."""
        params_1 = self._run_via_aer_and_get_init_params(
            sample_hamiltonian, mock_backend_config, seed=42
        )
        params_2 = self._run_via_aer_and_get_init_params(
            sample_hamiltonian, mock_backend_config, seed=42
        )
        np.testing.assert_array_equal(params_1, params_2)

    def test_different_seeds_produce_different_params_via_aer(
        self, mock_backend_config, sample_hamiltonian
    ):
        """Test that VQESolver.via_aer produces different init params with different seeds."""
        params_a = self._run_via_aer_and_get_init_params(
            sample_hamiltonian, mock_backend_config, seed=42
        )
        params_b = self._run_via_aer_and_get_init_params(
            sample_hamiltonian, mock_backend_config, seed=99
        )
        assert not np.array_equal(params_a, params_b)

    def test_seed_stored_in_init_data(self, mock_backend_config, sample_hamiltonian):
        """Test that the seed value is recorded in VQEInitialData."""
        self._run_via_aer_and_get_init_params(sample_hamiltonian, mock_backend_config, seed=42)
        # Re-run to check init_data.seed field
        solver = VQESolver(
            qubit_op=sample_hamiltonian,
            backend_config=mock_backend_config,
            max_iterations=1,
            optimizer='COBYLA',
            seed=42,
        )

        mock_backend = MagicMock(spec=AerBackend)
        mock_backend.name = 'aer_simulator'
        mock_backend.target = MagicMock()

        mock_ansatz_isa = MagicMock()
        mock_ansatz_isa.num_parameters = 16
        mock_ansatz_isa.layout = None

        mock_hamiltonian_isa = MagicMock()
        mock_hamiltonian_isa.num_qubits = sample_hamiltonian.num_qubits
        mock_hamiltonian_isa.to_list.return_value = []

        mock_minimize_result = MagicMock()
        mock_minimize_result.fun = -1.0
        mock_minimize_result.x = np.zeros(16)
        mock_minimize_result.success = True

        with (
            patch.object(
                solver, '_optimize_circuits', return_value=(mock_ansatz_isa, mock_hamiltonian_isa)
            ),
            patch('quantum_pipeline.solvers.vqe_solver.EstimatorV2'),
            patch(
                'quantum_pipeline.solvers.vqe_solver.minimize', return_value=mock_minimize_result
            ),
        ):
            solver.via_aer(mock_backend)

        assert solver.init_data.seed == 42

    def test_no_seed_produces_varying_params(self, mock_backend_config, sample_hamiltonian):
        """Test that VQESolver.via_aer without seed produces different params across runs."""
        params_1 = self._run_via_aer_and_get_init_params(
            sample_hamiltonian, mock_backend_config, seed=None
        )
        params_2 = self._run_via_aer_and_get_init_params(
            sample_hamiltonian, mock_backend_config, seed=None
        )
        # With no seed, params should almost certainly differ
        assert not np.array_equal(params_1, params_2)


class TestVQESolverHFInit:
    """Tests for Hartree-Fock initialization strategy via VQESolver."""

    @staticmethod
    def _run_via_aer_and_get_init_params(
        sample_hamiltonian, mock_backend_config, init_strategy='random', hf_data=None, seed=None
    ):
        """Helper: run via_aer with mocked optimizer/estimator, return initial_parameters."""
        mock_mapper = MagicMock()
        solver = VQESolver(
            qubit_op=sample_hamiltonian,
            backend_config=mock_backend_config,
            max_iterations=1,
            optimizer='COBYLA',
            seed=seed,
            init_strategy=init_strategy,
            hf_data=hf_data,
            mapper=mock_mapper,
        )

        mock_backend = MagicMock(spec=AerBackend)
        mock_backend.name = 'aer_simulator'
        mock_backend.target = MagicMock()

        mock_ansatz_isa = MagicMock()
        mock_ansatz_isa.num_parameters = 16
        mock_ansatz_isa.layout = None

        mock_hamiltonian_isa = MagicMock()
        mock_hamiltonian_isa.num_qubits = sample_hamiltonian.num_qubits
        mock_hamiltonian_isa.to_list.return_value = []

        mock_minimize_result = MagicMock()
        mock_minimize_result.fun = -1.0
        mock_minimize_result.x = np.zeros(16)
        mock_minimize_result.success = True

        # For HF init, mock the pre-optimization to avoid needing real quantum circuits
        hf_init_params = np.ones(16) * 0.5 if init_strategy == 'hf' and hf_data is not None else None

        with (
            patch.object(
                solver, '_optimize_circuits', return_value=(mock_ansatz_isa, mock_hamiltonian_isa)
            ),
            patch('quantum_pipeline.solvers.vqe_solver.EstimatorV2'),
            patch(
                'quantum_pipeline.solvers.vqe_solver.minimize', return_value=mock_minimize_result
            ),
        ):
            if hf_init_params is not None:
                with patch.object(
                    solver,
                    '_compute_hf_initial_parameters',
                    return_value=hf_init_params,
                ):
                    solver.via_aer(mock_backend)
            else:
                solver.via_aer(mock_backend)

        return solver.init_data

    def test_init_strategy_stored_in_init_data(self, mock_backend_config, sample_hamiltonian):
        """Test that init_strategy is recorded in VQEInitialData."""
        hf_data = HFData(num_particles=(1, 1), num_spatial_orbitals=2)
        init_data = self._run_via_aer_and_get_init_params(
            sample_hamiltonian, mock_backend_config, init_strategy='hf', hf_data=hf_data
        )
        assert init_data.init_strategy == 'hf'

    def test_random_strategy_stored(self, mock_backend_config, sample_hamiltonian):
        """Test that random strategy is recorded in VQEInitialData."""
        init_data = self._run_via_aer_and_get_init_params(
            sample_hamiltonian, mock_backend_config, init_strategy='random'
        )
        assert init_data.init_strategy == 'random'

    def test_hf_params_differ_from_random(self, mock_backend_config, sample_hamiltonian):
        """Test that HF params differ from random params."""
        hf_data = HFData(num_particles=(1, 1), num_spatial_orbitals=2)
        init_data_hf = self._run_via_aer_and_get_init_params(
            sample_hamiltonian, mock_backend_config, init_strategy='hf', hf_data=hf_data, seed=42
        )
        init_data_random = self._run_via_aer_and_get_init_params(
            sample_hamiltonian, mock_backend_config, init_strategy='random', seed=42
        )
        assert not np.array_equal(
            init_data_hf.initial_parameters, init_data_random.initial_parameters
        )

    def test_hf_fallback_when_no_hf_data(self, mock_backend_config, sample_hamiltonian):
        """Test that HF strategy falls back to random when hf_data is None."""
        init_data = self._run_via_aer_and_get_init_params(
            sample_hamiltonian, mock_backend_config, init_strategy='hf', hf_data=None, seed=42
        )
        # Should still produce params (random fallback), not crash
        assert init_data.initial_parameters is not None
        assert len(init_data.initial_parameters) > 0

    def test_build_ansatz_is_plain_esu2(self, mock_backend_config, sample_hamiltonian):
        """Test that _build_ansatz always builds a plain EfficientSU2 (no HF circuit prepend)."""
        hf_data = HFData(num_particles=(1, 1), num_spatial_orbitals=2)
        mock_mapper = MagicMock()
        solver = VQESolver(
            qubit_op=sample_hamiltonian,
            backend_config=mock_backend_config,
            init_strategy='hf',
            hf_data=hf_data,
            mapper=mock_mapper,
        )
        with patch('quantum_pipeline.solvers.vqe_solver.EfficientSU2') as mock_esu2:
            solver._build_ansatz(4)
            mock_esu2.assert_called_once()
            _, kwargs = mock_esu2.call_args
            assert 'initial_state' not in kwargs

    def test_build_ansatz_without_hf(self, mock_backend_config, sample_hamiltonian):
        """Test that _build_ansatz does not pass initial_state for random strategy."""
        solver = VQESolver(
            qubit_op=sample_hamiltonian,
            backend_config=mock_backend_config,
            init_strategy='random',
        )
        with patch('quantum_pipeline.solvers.vqe_solver.EfficientSU2') as mock_esu2:
            solver._build_ansatz(4)
            mock_esu2.assert_called_once()
            _, kwargs = mock_esu2.call_args
            assert 'initial_state' not in kwargs
