#  Copyright 2026 European Union
#  Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
#  SPDX-License-Identifier: EUPL-1.2
"""Comprehensive unit tests for the SteeringBuilder."""

from __future__ import annotations

from pathlib import Path
from textwrap import dedent
from typing import Any
from unittest.mock import patch

import pytest
import tomlkit
from tomlkit import document, table
from tomlkit.items import Table

from mafw.db.db_configurations import default_conf
from mafw.steering.builder import SteeringBuilder, ValidationLevel
from mafw.steering.models import (
    ConditionalFilterConfig,
    FieldFilterConfig,
    ModelFilterConfig,
)


class TestSteeringBuilderBasics:
    """Test initialization and basic attribute access."""

    def test_builder_defaults(self) -> None:
        """A fresh builder should have standard default values."""
        builder = SteeringBuilder()
        assert builder.globals.analysis_name == 'analysis-name'
        assert builder.globals.analysis_description == 'analysis-description'
        assert builder.globals.new_only is True
        assert builder.globals.create_standard_tables is True
        assert builder.db_config.url == 'sqlite:///:memory:'
        assert builder.db_config.pragmas == default_conf['sqlite']['pragmas']
        assert builder.ui_config.interface == 'rich'
        assert builder.db_config.enabled is True

    def test_list_methods_initially_empty(self) -> None:
        builder = SteeringBuilder()
        assert builder.list_processors() == []
        assert builder.list_groups() == []
        assert builder.extra_globals == {}
        assert builder.document is None

    def test_ensure_str_list_handles_none(self) -> None:
        builder = SteeringBuilder()
        assert builder._ensure_str_list(None) == []


class TestSteeringBuilderProcessorManagement:
    """Test adding, removing, and configuring processors and replicas."""

    def test_add_processor_and_parameter(self) -> None:
        builder = SteeringBuilder()
        builder.add_processor('MyProcessor')
        builder.set_parameter('MyProcessor', 'threshold', 42)

        assert 'MyProcessor' in builder.globals.processors_to_run
        proc = builder.get_processor_config('MyProcessor')
        assert proc.parameters['threshold'].value == 42

    def test_add_processor_duplicate_is_noop(self) -> None:
        builder = SteeringBuilder()
        builder.add_processor('Repeat')
        builder.add_processor('Repeat')
        assert builder.globals.processors_to_run.count('Repeat') == 1

    def test_add_processor_with_replica_registers_both(self) -> None:
        builder = SteeringBuilder()
        builder.add_processor('Primary', 'variant')
        assert builder.globals.processors_to_run == ['Primary#variant']
        assert 'Primary' in builder.list_processors()
        assert 'Primary#variant' in builder.list_processors()

    def test_remove_processor(self) -> None:
        builder = SteeringBuilder()
        builder.add_processor('ToDrop')
        assert 'ToDrop' in builder.globals.processors_to_run
        builder.remove_processor('ToDrop')
        assert 'ToDrop' not in builder.globals.processors_to_run

    def test_set_processors_to_run_accepts_iterables(self) -> None:
        builder = SteeringBuilder()
        builder.set_processors_to_run(('A', 'B'))
        assert builder.globals.processors_to_run == ['A', 'B']

    def test_replica_management(self) -> None:
        builder = SteeringBuilder()
        builder.add_replica('Base', 'variant')
        builder.set_replica_inheritance('Base#variant', False)
        builder.set_parameter('Base#variant', 'special', True)

        proc = builder.get_processor_config('Base#variant')
        assert proc.parameters['special'].value is True
        assert proc.inheritance is False

    def test_processor_new_only(self) -> None:
        builder = SteeringBuilder()
        builder.add_processor('Proc')
        builder.set_processor_new_only('Proc', True)
        assert builder.get_processor_config('Proc').new_only is True


class TestSteeringBuilderFilterManagement:
    """Test configuration of filters and conditionals."""

    @pytest.fixture
    def builder(self) -> SteeringBuilder:
        b = SteeringBuilder()
        b.add_processor('Proc')
        return b

    def test_filter_crud(self, builder: SteeringBuilder) -> None:
        # Set config
        builder.set_filter_config('Proc', 'ModelA', {'alpha': 1})
        model_filters = builder.get_processor_config('Proc').filters['ModelA']
        model_config = next(f for f in model_filters if isinstance(f, ModelFilterConfig))
        assert model_config.conditions['alpha'].value == 1

        # Set field
        builder.set_filter_field('Proc', 'ModelA', 'beta', 2)
        assert model_config.conditions['beta'].value == 2

        # Set logic
        builder.set_filter_logic('Proc', 'alpha AND beta')
        assert builder.get_processor_config('Proc').filter_logic == 'alpha AND beta'

        # Set conditionals
        conds = [{'condition_field': 'beta', 'condition_op': 'eq', 'condition_value': 2}]
        builder.set_filter_conditionals('Proc', 'ModelA', conds)
        assert any(
            isinstance(f, ConditionalFilterConfig) for f in builder.get_processor_config('Proc').filters['ModelA']
        )

        # Remove conditionals
        builder.set_filter_conditionals('Proc', 'ModelA', None)
        assert not any(
            isinstance(f, ConditionalFilterConfig) for f in builder.get_processor_config('Proc').filters['ModelA']
        )

        # Remove filter
        builder.remove_filter('Proc', 'ModelA')
        assert 'ModelA' not in builder.get_processor_config('Proc').filters

    def test_remove_non_existent_filter_is_safe(self, builder: SteeringBuilder) -> None:
        builder.remove_filter('Proc', 'NoSuchModel')


class TestSteeringBuilderGlobalSettings:
    """Test top-level global settings and UI/DB toggles."""

    def test_global_mutators(self) -> None:
        builder = SteeringBuilder()
        builder.set_analysis_name('NewName')
        builder.set_analysis_description('NewDesc')
        builder.set_new_only(False)
        builder.set_create_standard_tables(False)

        assert builder.globals.analysis_name == 'NewName'
        assert builder.globals.analysis_description == 'NewDesc'
        assert builder.globals.new_only is False
        assert builder.globals.create_standard_tables is False

    def test_db_configuration_toggles(self) -> None:
        builder = SteeringBuilder()
        assert builder.is_db_configuration_enabled() is True
        builder.disable_db_configuration()
        assert builder.is_db_configuration_enabled() is False
        builder.enable_db_configuration()
        assert builder.is_db_configuration_enabled() is True

    def test_db_settings(self) -> None:
        builder = SteeringBuilder()
        builder.set_db_url('postgresql://user:pass@host/db')
        assert builder.db_config.url == 'postgresql://user:pass@host/db'
        assert builder.db_config.attributes['URL'] == 'postgresql://user:pass@host/db'

        builder.set_db_url(None)
        assert builder.db_config.url is None
        assert 'URL' not in builder.db_config.attributes

        builder.set_db_pragmas({'cache': 100})
        assert builder.db_config.pragmas == {'cache': 100}

        builder.set_db_attribute('custom_attr', 'val')
        assert builder.db_config.attributes['custom_attr'] == 'val'
        builder.set_db_attribute('custom_attr', None)
        assert 'custom_attr' not in builder.db_config.attributes

    def test_ui_interface_setting(self) -> None:
        builder = SteeringBuilder()
        builder.set_ui_interface('console')
        assert builder.ui_config.interface == 'console'


class TestSteeringBuilderGroups:
    """Test processor group management."""

    def test_group_crud(self) -> None:
        builder = SteeringBuilder()
        builder.add_group('MyGroup', ['P1', 'P2'], 'A group description')
        assert 'MyGroup' in builder.list_groups()
        group = builder.get_group('MyGroup')
        assert group.processors == ['P1', 'P2']
        assert group.description == 'A group description'

        builder.remove_group('MyGroup')
        assert 'MyGroup' not in builder.list_groups()


class TestSteeringBuilderPersistence:
    """Test loading, writing, and serializing TOML."""

    def test_from_toml_str_path(self, tmp_path: Path) -> None:
        content = dedent("""
            analysis_name = "test"
            [Proc]
            param = 1
        """).strip()
        p = tmp_path / 'test.toml'
        p.write_text(content)

        builder = SteeringBuilder.from_toml(str(p))
        assert builder.globals.analysis_name == 'test'
        assert 'Proc' in builder.processors

    def test_from_toml_no_db_disables_it(self, tmp_path: Path) -> None:
        p = tmp_path / 'no_db.toml'
        p.write_text('analysis_name = "no-db"')
        builder = SteeringBuilder.from_toml(p)
        assert builder.db_config.enabled is False

    def test_write_and_reload_roundtrip(self, tmp_path: Path) -> None:
        builder = SteeringBuilder()
        builder.set_analysis_name('roundtrip')
        builder.add_processor('Proc')
        builder.set_filter_field('Proc', 'Model', 'f', 10)

        p = tmp_path / 'out.toml'
        builder.write(p)

        reloaded = SteeringBuilder.from_toml(p)
        assert reloaded.globals.analysis_name == 'roundtrip'
        model_filters = reloaded.get_processor_config('Proc').filters['Model']
        model_config = next(f for f in model_filters if isinstance(f, ModelFilterConfig))
        assert model_config.conditions['f'].value == 10

    def test_to_document_with_disabled_db(self) -> None:
        builder = SteeringBuilder()
        builder.disable_db_configuration()
        doc = builder.to_document()
        assert 'DBConfiguration' not in doc

    def test_to_config_dict(self) -> None:
        builder = SteeringBuilder()
        d = builder.to_config_dict()
        assert isinstance(d, dict)
        assert d['analysis_name'] == 'analysis-name'

    def test_to_config_dict_from_existing_document(self, tmp_path: Path) -> None:
        p = tmp_path / 'existing.toml'
        p.write_text('extra = 42')
        builder = SteeringBuilder.from_toml(p)
        assert builder.to_config_dict()['extra'] == 42


class TestSteeringBuilderParsingInternals:
    """Test internal parsing logic for complex TOML structures."""

    def test_parse_inline_filter_definitions(self) -> None:
        builder = SteeringBuilder()
        doc = document()
        proc_table = table()
        filter_table = table()
        filter_table['__logic__'] = 'OR'
        model_table = table()
        model_table['key'] = 'val'
        filter_table['Model'] = model_table
        proc_table['__filter__'] = filter_table
        doc['Proc'] = proc_table

        builder._load_from_document(doc)
        config = builder.get_processor_config('Proc')
        assert config.filter_logic == 'OR'
        model_filters = config.filters['Model']
        model_config = next(f for f in model_filters if isinstance(f, ModelFilterConfig))
        assert model_config.conditions['key'].value == 'val'

    def test_parse_filter_section_short_prefix(self) -> None:
        builder = SteeringBuilder()
        # Should just ensure the processor exists
        builder._parse_filter_section(['P', 'too_short'], table())
        assert 'P' in builder.list_processors()

    def test_parse_filter_section_nested_path(self) -> None:
        builder = SteeringBuilder()
        t = table()
        t['val'] = 1
        builder._parse_filter_section(['Proc', '__filter__', 'Model'], t)
        model_filters = builder.get_processor_config('Proc').filters['Model']
        model_config = next(f for f in model_filters if isinstance(f, ModelFilterConfig))
        assert model_config.conditions['val'].value == 1

    def test_parse_filter_section_enable_flags(self) -> None:
        builder = SteeringBuilder()
        doc = document()
        proc_table = table()
        filter_table = table()
        model_table = table()
        model_table['__enable__'] = False
        model_table['field_a'] = 1
        model_table['FieldFilter'] = {'__enable__': False, 'f': 2}
        model_table['__conditional__'] = [
            {
                '__enable__': False,
                'condition_field': 'c',
                'condition_op': '==',
                'condition_value': 1,
                'then_field': 't',
                'then_op': '==',
                'then_value': 2,
            }
        ]
        filter_table['Model'] = model_table
        proc_table['__filter__'] = filter_table
        doc['Proc'] = proc_table

        builder._load_from_document(doc)
        filters = builder.get_processor_config('Proc').filters['Model']
        model_config = next(f for f in filters if isinstance(f, ModelFilterConfig))
        field_config = next(f for f in filters if isinstance(f, FieldFilterConfig))
        cond_config = next(f for f in filters if isinstance(f, ConditionalFilterConfig))
        assert model_config.enabled is False
        assert field_config.enabled is False
        assert cond_config.enabled is False

    def test_parse_filter_section_root_logic(self) -> None:
        builder = SteeringBuilder()
        t = table()
        t['__logic__'] = 'X'
        builder._parse_filter_section(['Proc', '__filter__'], t)
        assert builder.get_processor_config('Proc').filter_logic == 'X'

    def test_parse_processor_special_keys(self) -> None:
        builder = SteeringBuilder()
        t = table()
        t['__logic__'] = 'L'
        t['__new_only__'] = True
        t['__inheritance__'] = False
        t['custom'] = 'C'
        builder._parse_processor('Proc', t)

        config = builder.get_processor_config('Proc')
        assert config.filter_logic == 'L'
        assert config.new_only is True
        assert config.inheritance is False
        assert config.parameters['custom'].value == 'C'

    def test_load_from_document_with_various_sections(self) -> None:
        doc = document()
        doc['extra'] = 'global'
        doc['processors_to_run'] = ['P']

        # Flat filter section
        filter_table = table()
        filter_table['__logic__'] = 'AND'
        doc['P.__filter__'] = filter_table

        # Nested filter section
        model_table = table()
        model_table['f'] = 1
        doc['P.__filter__.Model'] = model_table

        db = table()
        db['URL'] = 'url'
        db_pragmas = table()
        db_pragmas['p'] = 1
        db['pragmas'] = db_pragmas
        doc['DBConfiguration'] = db

        ui = table()
        ui['interface'] = 'cli'
        doc['UserInterface'] = ui

        grp = table()
        grp['processors_to_run'] = ['P']
        grp['description'] = 'D'
        grp['other'] = 'O'
        doc['MyGroup'] = grp

        builder = SteeringBuilder()
        builder._load_from_document(doc)

        assert builder.extra_globals['extra'] == 'global'
        assert builder.db_config.url == 'url'
        assert builder.db_config.pragmas['p'] == 1
        assert builder.ui_config.interface == 'cli'
        assert builder.groups['MyGroup'].description == 'D'
        assert builder.groups['MyGroup'].attributes['other'] == 'O'

    @pytest.mark.parametrize(
        'value, expected',
        [
            (table(), {}),
            (tomlkit.array('[1, 2]'), [1, 2]),
            (tomlkit.integer(5), 5),
        ],
    )
    def test_toml_to_python(self, value: Any, expected: Any) -> None:
        builder = SteeringBuilder()
        assert builder._toml_to_python(value) == expected


class TestSteeringBuilderEdgeCases:
    """Tests targeting specific internal branches for 100% coverage."""

    def test_parse_ui_config_without_interface(self) -> None:
        builder = SteeringBuilder()
        builder.ui_config.interface = 'original'
        builder._parse_ui_config(table())
        assert builder.ui_config.interface == 'original'

    def test_parse_filter_section_without_logic(self) -> None:
        builder = SteeringBuilder()
        builder._parse_filter_section(['Proc', '__filter__'], table())
        assert builder.get_processor_config('Proc').filter_logic is None

    def test_load_filters_with_non_table_entry(self) -> None:
        builder = SteeringBuilder()
        t = table()
        t['valid_model'] = table()
        t['invalid_entry'] = 123  # This should trigger the continue in _load_filters
        t['__logic__'] = 'AND'

        filters = builder._load_filters(t)
        assert 'valid_model' in filters
        assert 'invalid_entry' not in filters
        assert '__logic__' not in filters

    def test_parse_filter_section_with_special_entries(self) -> None:
        builder = SteeringBuilder()
        t = table()
        t['__logic__'] = 'OR'
        t['not_a_table'] = 456
        t['Model'] = table()
        t['Model']['f'] = 1

        # This will hit the if parts[-1] == '__filter__' branch
        builder._parse_filter_section(['Proc', '__filter__'], t)

        config = builder.get_processor_config('Proc')
        assert config.filter_logic == 'OR'
        assert 'Model' in config.filters
        assert 'not_a_table' not in config.filters

    def test_ensure_str_list_with_various_inputs(self) -> None:
        builder = SteeringBuilder()
        assert builder._ensure_str_list(None) == []
        assert builder._ensure_str_list(['a', 1]) == ['a', '1']

    def test_toml_to_python_with_item(self) -> None:
        builder = SteeringBuilder()
        item = tomlkit.integer(10)
        assert builder._toml_to_python(item) == 10


class TestSteeringBuilderCoverage:
    """Additional tests to optimize coverage for SteeringBuilder."""

    def test_builder_model_logic_coverage(self):
        """Cover builder.py: logic in model_data and missing logic branch."""
        # 1. With logic
        doc = document()
        proc_table = table()
        filter_table = table()
        model_table = table()
        model_table['__logic__'] = 'a AND b'
        model_table['a'] = 1
        model_table['b'] = 2
        filter_table['Model'] = model_table
        proc_table['__filter__'] = filter_table
        doc['Proc'] = proc_table

        builder = SteeringBuilder()
        builder._load_from_document(doc)

        filters = builder.get_processor_config('Proc').filters['Model']
        model_config = next(f for f in filters if isinstance(f, ModelFilterConfig))
        assert model_config.logic == 'a AND b'

        # 2. Without logic (missing branch)
        builder.set_filter_config('Proc', 'Model2', {'f': 1})
        filters2 = builder.get_processor_config('Proc').filters['Model2']
        model_config2 = next(f for f in filters2 if isinstance(f, ModelFilterConfig))
        assert model_config2.logic is None

    def test_builder_conditional_config_list_coverage(self):
        """Cover builder.py: __conditional__ non-list branch."""
        builder = SteeringBuilder()
        # Case 1: non-list __conditional__ in 3-part section
        t = table()
        t['__conditional__'] = 123
        builder._parse_filter_section(['Proc', '__filter__', 'Model'], t)
        filters = builder.get_processor_config('Proc').filters['Model']
        assert not any(isinstance(f, ConditionalFilterConfig) for f in filters)

        # Case 2: non-list __conditional__ in root __filter__ section (_load_filters)
        doc = document()
        p = table()
        f = table()
        m = table()
        m['__conditional__'] = 456
        f['Model'] = m
        p['__filter__'] = f
        doc['Proc'] = p
        builder._load_from_document(doc)
        filters2 = builder.get_processor_config('Proc').filters['Model']
        assert not any(isinstance(f, ConditionalFilterConfig) for f in filters2)

    def test_builder_conditional_config_partial_data(self):
        """Cover builder.py: missing condition_op/value or else_op/value."""
        builder = SteeringBuilder()
        # Case 1: Missing condition
        data1 = {'then_field': 'f', 'then_op': '==', 'then_value': 1}
        config1 = builder._parse_conditional_config('Model', data1)
        assert config1.condition is None
        assert config1.then_clause is not None

        # 2. Present else
        data2 = {
            'condition_field': 'c',
            'condition_op': '==',
            'condition_value': 0,
            'then_field': 't',
            'then_op': '==',
            'then_value': 1,
            'else_field': 'e',
            'else_op': '==',
            'else_value': 2,
        }
        config2 = builder._parse_conditional_config('Model', data2)
        assert config2.else_clause is not None

    def test_set_processor_filters_coverage(self):
        """Cover set_processor_filters in builder.py."""
        builder = SteeringBuilder()
        builder.add_processor('Proc')
        filters = {'Model': [ModelFilterConfig(name='Model', model='Model')]}
        builder.set_processor_filters('Proc', filters, 'Model')
        config = builder.get_processor_config('Proc')
        assert config.filters == filters
        assert config.filter_logic == 'Model'

    def test_parse_condition_types(self):
        """Cover different condition types in _parse_condition."""
        builder = SteeringBuilder()
        # List -> IN
        c1 = builder._parse_condition([1, 2])
        assert c1.operator == 'IN'
        # String -> GLOB
        c2 = builder._parse_condition('pattern*')
        assert c2.operator == 'GLOB'
        # Explicit dict -> custom op
        c3 = builder._parse_condition({'op': '>=', 'value': 10})
        assert c3.operator == '>='
        assert c3.value == 10
        assert c3.is_implicit is False
        # Other -> == (already covered but for completeness)
        c4 = builder._parse_condition(123)
        assert c4.operator == '=='

    def test_load_filters_more_coverage(self):
        """Cover conditionals and field filters in _load_filters via root __filter__."""
        builder = SteeringBuilder()
        doc = document()
        p = table()
        f = table()
        m = table()
        # Logic with parse error to cover line 221
        m['__logic__'] = 'INVALID('
        # Cover line 232-233 (conditionals) in _load_filters
        m['__conditional__'] = [
            {
                'condition_field': 'c',
                'condition_op': '==',
                'condition_value': 1,
                'then_field': 't',
                'then_op': '==',
                'then_value': 2,
            }
        ]
        # Cover line 239 (sub-table) in _load_filters
        m['SubField'] = {'f': 1}
        f['Model'] = m
        p['__filter__'] = f
        doc['Proc'] = p
        builder._load_from_document(doc)

        config = builder.get_processor_config('Proc')
        filters = config.filters['Model']
        model_config = next(f for f in filters if isinstance(f, ModelFilterConfig))
        assert model_config.logic_ast is None
        assert any(isinstance(f, ConditionalFilterConfig) for f in filters)
        assert any(isinstance(f, FieldFilterConfig) for f in filters)

    def test_remove_processor_not_found(self):
        """Cover line 380 False branch in remove_processor."""
        builder = SteeringBuilder()
        builder.remove_processor('NonExistent')

    def test_parse_filter_section_model_logic_error(self):
        """Cover line 343 parsing error branch in _parse_filter_section."""
        builder = SteeringBuilder()
        t = table()
        t['__logic__'] = 'INVALID('
        builder._parse_filter_section(['Proc', '__filter__', 'Model'], t)
        filters = builder.get_processor_config('Proc').filters['Model']
        model_config = next(f for f in filters if isinstance(f, ModelFilterConfig))
        assert model_config.logic_ast is None

    def test_builder_parse_filter_section_logic(self):
        """Cover builder.py: _parse_filter_section logic and model loading."""
        builder = SteeringBuilder()

        # 1. __filter__ root logic
        t = table()
        t['__logic__'] = 'L'
        m = table()
        m['f'] = 1
        t['Model'] = m
        builder._parse_filter_section(['Proc', '__filter__'], t)
        assert builder.get_processor_config('Proc').filter_logic == 'L'
        assert 'Model' in builder.get_processor_config('Proc').filters

        # 2. Model level logic and conditionals via 3-part name
        t2 = table()
        t2['__logic__'] = 'ML'
        t2['__conditional__'] = [
            {
                'condition_field': 'c',
                'condition_op': '==',
                'condition_value': 1,
                'then_field': 't',
                'then_op': '==',
                'then_value': 2,
            }
        ]
        t2['sub'] = {'f': 1}
        builder._parse_filter_section(['Proc', '__filter__', 'Model2'], t2)

        config = builder.get_processor_config('Proc')
        filters = config.filters['Model2']
        model_config = next(f for f in filters if isinstance(f, ModelFilterConfig))
        assert model_config.logic == 'ML'
        assert any(isinstance(f, ConditionalFilterConfig) for f in filters)
        assert any(isinstance(f, FieldFilterConfig) for f in filters)

    def test_builder_set_parameter_coverage(self):
        """Cover builder.py: set_parameter branches."""
        builder = SteeringBuilder()
        builder.add_processor('Proc')

        # New parameter
        builder.set_parameter('Proc', 'new_p', 1)
        assert builder.get_processor_config('Proc').parameters['new_p'].value == 1

        # Existing parameter
        builder.set_parameter('Proc', 'new_p', 2)
        assert builder.get_processor_config('Proc').parameters['new_p'].value == 2

        # Remove parameter
        builder.remove_parameter('Proc', 'new_p')
        assert 'new_p' not in builder.get_processor_config('Proc').parameters

        # Clear parameters
        builder.set_parameter('Proc', 'p1', 1)
        builder.clear_parameters('Proc')
        assert len(builder.get_processor_config('Proc').parameters) == 0

    def test_builder_set_filter_config_branches(self):
        """Cover builder.py: set_filter_config branches."""
        builder = SteeringBuilder()
        builder.add_processor('Proc')

        # set_filter_config with logic, conditionals (list and non-list), and sub-tables
        config = {
            '__logic__': 'L',
            '__conditional__': [
                {
                    'condition_field': 'c',
                    'condition_op': '==',
                    'condition_value': 1,
                    'then_field': 't',
                    'then_op': '==',
                    'then_value': 2,
                }
            ],
            'field_f': {'f1': 1},
        }
        builder.set_filter_config('Proc', 'Model', config)

        filters = builder.get_processor_config('Proc').filters['Model']
        model_config = next(f for f in filters if isinstance(f, ModelFilterConfig))
        assert model_config.logic == 'L'
        assert any(isinstance(f, ConditionalFilterConfig) for f in filters)
        assert any(isinstance(f, FieldFilterConfig) for f in filters)

        # set_filter_config with non-list conditionals
        config2 = {'__conditional__': 123}
        builder.set_filter_config('Proc', 'Model2', config2)
        filters2 = builder.get_processor_config('Proc').filters['Model2']
        assert not any(isinstance(f, ConditionalFilterConfig) for f in filters2)

    def test_builder_misc_coverage(self):
        """Cover other missing lines in builder.py."""
        builder = SteeringBuilder()
        builder.add_processor('Proc')

        # add_processor duplicate return
        builder.add_processor('Proc')

        # remove_processor remove call
        builder.remove_processor('Proc')
        assert 'Proc' not in builder.globals.processors_to_run

        # set_processors_to_run
        builder.set_processors_to_run(['P1', 'P2'])
        assert builder.globals.processors_to_run == ['P1', 'P2']

        # set_filter_field where model_config is None (forced)
        builder.add_processor('Proc2')
        builder.get_processor_config('Proc2').filters['Model'] = []
        builder.set_filter_field('Proc2', 'Model', 'f', 1)
        filters = builder.get_processor_config('Proc2').filters['Model']
        assert isinstance(filters[0], ModelFilterConfig)

    def test_builder_invalid_logic_parsing_coverage(self):
        """Cover invalid logic parsing in various builder methods."""
        builder = SteeringBuilder()

        # 1. _parse_processor via _load_from_document
        doc = document()
        p = table()
        p['__logic__'] = 'INVALID('
        doc['Proc'] = p
        builder._load_from_document(doc)
        assert builder.get_processor_config('Proc').logic_ast is None

        # 2. _load_filters via set_filter_config
        builder.set_filter_config('Proc', 'Model', {'__logic__': 'INVALID('})
        filters = builder.get_processor_config('Proc').filters['Model']
        model_config = next(f for f in filters if isinstance(f, ModelFilterConfig))
        assert model_config.logic_ast is None

        # 3. _parse_field_filter_config via set_filter_config
        builder.set_filter_config('Proc', 'Model', {'Field': {'__logic__': 'INVALID('}})
        filters = builder.get_processor_config('Proc').filters['Model']
        field_config = next(f for f in filters if isinstance(f, FieldFilterConfig))
        assert field_config.logic_ast is None

        # 4. _parse_filter_section (root __filter__)
        t = table()
        t['__logic__'] = 'INVALID('
        builder._parse_filter_section(['Proc2', '__filter__'], t)
        assert builder.get_processor_config('Proc2').logic_ast is None

        # 5. _parse_filter_section (model level __filter__)
        t2 = table()
        t2['__logic__'] = 'INVALID('
        builder._parse_filter_section(['Proc2', '__filter__', 'Model'], t2)
        filters2 = builder.get_processor_config('Proc2').filters['Model']
        model_config2 = next(f for f in filters2 if isinstance(f, ModelFilterConfig))
        assert model_config2.logic_ast is None

        # 6. set_filter_logic
        builder.set_filter_logic('Proc', 'INVALID(')
        assert builder.get_processor_config('Proc').logic_ast is None
        builder.set_filter_logic('Proc', None)
        assert builder.get_processor_config('Proc').logic_ast is None

        # 7. _load_filters via _parse_processor (different TOML structure)
        doc2 = document()
        p2 = table()
        f2 = table()
        m2 = table()
        m2['__logic__'] = 'INVALID('
        f2['Model'] = m2
        p2['__filter__'] = f2
        doc2['Proc3'] = p2
        builder._load_from_document(doc2)
        filters3 = builder.get_processor_config('Proc3').filters['Model']
        model_config3 = next(f for f in filters3 if isinstance(f, ModelFilterConfig))
        assert model_config3.logic_ast is None

    def test_builder_none_logic_coverage(self):
        """Cover None logic branches (is not None -> else)."""
        builder = SteeringBuilder()
        builder.add_processor('Proc')

        # 1. set_filter_config (__logic__=None)
        builder.set_filter_config('Proc', 'Model', {'__logic__': None})

        # 2. set_filter_logic (None)
        builder.set_filter_logic('Proc', None)

    def test_builder_parse_processor_inheritance_new_only(self):
        """Cover __inheritance__ and __new_only__ in _parse_processor."""
        builder = SteeringBuilder()
        doc = document()
        p = table()
        p['__inheritance__'] = True
        p['__new_only__'] = False
        doc['Proc'] = p
        builder._load_from_document(doc)
        config = builder.get_processor_config('Proc')
        assert config.inheritance is True
        assert config.new_only is False

    def test_builder_parse_db_config_pragmas_none(self):
        """Cover _parse_db_config pragmas branch."""
        builder = SteeringBuilder()
        doc = document()
        db = table()
        db['URL'] = 'sqlite://'
        # No pragmas table
        doc['DBConfiguration'] = db
        builder._load_from_document(doc)
        assert builder.db_config.url == 'sqlite://'


class TestSteeringBuilderValidation:
    """Test validation method."""

    def test_validate_calls_internal_validate(self) -> None:
        builder = SteeringBuilder()
        with patch('mafw.steering.validation.validate', return_value=[]) as mock_val:
            issues = builder.validate(ValidationLevel.FULL)
            assert issues == []
            mock_val.assert_called_once_with(builder, ValidationLevel.FULL)


class TestSteeringBuilderCoverageExtra:
    """Extra tests to fill coverage gaps in builder."""

    def test_logic_none_branch_coverage(self) -> None:
        """Covers if model_filter.logic_str_original is not None: branch (False)."""
        builder = SteeringBuilder()

        # Case 223: in _load_filters
        with patch.object(builder, '_toml_to_python') as mock_toml:
            # When called for the model table, return a dict with __logic__ = None
            # When called for anything else (like items in the table), return as is
            mock_toml.side_effect = lambda x: {'__logic__': None} if isinstance(x, Table) else x

            t = tomlkit.table()
            t['Model'] = tomlkit.table()
            filters = builder._load_filters(t)
            assert filters['Model'][0].logic_str_original is None

        # Case 284: in _parse_field_filter_config
        with patch.object(builder, '_toml_to_python') as mock_toml:
            # Similar trick for field filter
            mock_toml.side_effect = lambda x: {'MyField': {'__logic__': None}} if isinstance(x, Table) else x
            t = tomlkit.table()
            t['Model'] = tomlkit.table()
            filters = builder._load_filters(t)
            # Find the field filter
            field_filter = next(f for f in filters['Model'] if isinstance(f, FieldFilterConfig))
            assert field_filter.logic_str_original is None

        # Case 351: in _parse_filter_section
        with patch.object(builder, '_toml_to_python') as mock_toml:
            mock_toml.side_effect = lambda x: {'__logic__': None} if isinstance(x, Table) else x
            builder._parse_filter_section(['P', '__filter__', 'Model'], tomlkit.table())
            proc = builder.get_processor_config('P')
            assert proc.filters['Model'][0].logic_str_original is None

        # Case 303 (not requested but good to have): in _parse_filter_section root logic
        builder._parse_filter_section(['P_root', '__filter__'], tomlkit.table())
        assert builder.get_processor_config('P_root').logic_str_original is None

        # Case 458: in set_filter_config
        builder.set_filter_config('P4', 'M4', {'__logic__': None})
        assert builder.get_processor_config('P4').filters['M4'][0].logic_str_original is None

    def test_filter_enabled_branch_coverage(self) -> None:
        """Covers lines 347 and 454 (enabled=False)."""
        builder = SteeringBuilder()

        # Case 454: set_filter_config
        builder.set_filter_config('P', 'M', {'__enable__': False})
        proc = builder.get_processor_config('P')
        assert proc.filters['M'][0].enabled is False

        # Case 347: _parse_filter_section
        doc = tomlkit.document()
        # [P.__filter__.M2]
        # __enable__ = false
        m2_table = tomlkit.table()
        m2_table['__enable__'] = False
        doc['P.__filter__.M2'] = m2_table

        builder2 = SteeringBuilder()
        builder2._load_from_document(doc)
        proc2 = builder2.get_processor_config('P')
        assert proc2.filters['M2'][0].enabled is False


def test_builder_set_logic_expression_empty() -> None:
    """Trigger the empty text branches in _set_logic_expression."""
    builder = SteeringBuilder()
    builder.add_processor('Proc')
    proc = builder.get_processor_config('Proc')

    # 1. logic_text is None
    builder._set_logic_expression(proc, None)
    assert proc.logic_ast is None
    assert proc.logic_is_valid is True

    # 2. logic_text is empty string
    builder._set_logic_expression(proc, '   ')
    assert proc.logic_ast is None
    assert proc.logic_is_valid is True


def test_builder_invalid_logic_exception() -> None:
    """Trigger the except block in _set_logic_expression."""
    builder = SteeringBuilder()
    builder.add_processor('Any')
    # Providing something that ExprParser will fail on
    builder._set_logic_expression(builder.get_processor_config('Any'), 'INVALID !! OR')
    assert builder.get_processor_config('Any').logic_is_valid is False
