#  Copyright 2026 European Union
#  Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
#  SPDX-License-Identifier: EUPL-1.2
"""Serialization and roundtrip tests."""

from pathlib import Path

import pytest

import mafw.mafw_errors
from mafw.enumerators import LogicalOp
from mafw.steering.builder import SteeringBuilder, ValidationLevel
from mafw.steering.models import (
    Condition,
    ConditionalFilterConfig,
    FieldFilterConfig,
    ModelFilterConfig,
    ProcessorConfig,
)
from mafw.steering.serializer import _build_conditional_array, _normalize_logical_op, _serialize_condition, serialize
from mafw.tools.toml_tools import load_steering_file, load_steering_file_legacy


def test_minimal_steering_roundtrip(tmp_path: Path) -> None:
    builder = SteeringBuilder()
    builder.add_processor('MainProc')
    builder.set_analysis_name('test-run')
    builder.set_filter_field('MainProc', 'ModelX', 'flag', True)
    builder.set_filter_conditionals(
        'MainProc',
        'ModelX',
        [{'condition_field': 'flag', 'condition_op': 'eq', 'condition_value': 1}],
    )
    builder.add_group('GroupA', ['MainProc'])
    output = tmp_path / 'steering.toml'
    builder.write(output)

    recreated = SteeringBuilder.from_toml(output)
    assert recreated.globals.analysis_name == 'test-run'

    # Updated assertion for new object-based filter structure
    model_filters = recreated.get_processor_config('MainProc').filters['ModelX']
    model_config = next(f for f in model_filters if isinstance(f, ModelFilterConfig))
    assert model_config.conditions['flag'].value is True

    assert 'GroupA' in recreated.groups


def test_serialization_replica_and_database(tmp_path: Path) -> None:
    builder = SteeringBuilder()
    builder.add_processor('Alpha')
    builder.add_replica('Alpha', 'r1')
    builder.set_processor_new_only('Alpha', True)
    builder.set_replica_inheritance('Alpha#r1', False)
    builder.set_filter_logic('Alpha', 'alpha AND beta')
    builder.set_filter_conditionals(
        'Alpha',
        'ModelY',
        [{'condition_field': 'alpha', 'condition_op': 'eq', 'condition_value': 1}],
    )
    builder.set_db_url('sqlite:///coverage.db')
    builder.set_db_pragmas({'journal_mode': 'wal'})
    builder.set_analysis_description('pipeline')
    builder.set_new_only(True)
    builder.set_create_standard_tables(False)
    builder.set_ui_interface('console')
    builder.globals.processors_to_run = ['Alpha', 'Alpha#r1', 'Pipeline']
    builder.add_group('Pipeline', ['Alpha', 'Alpha#r1'], 'grouped')
    doc = builder.to_document()

    assert doc['DBConfiguration']['URL'] == 'sqlite:///coverage.db'
    assert doc['DBConfiguration']['pragmas']['journal_mode'] == 'wal'
    assert doc['Alpha']['__new_only__'] is True
    assert doc['Alpha#r1']['__inheritance__'] is False
    assert doc['Alpha']['__filter__']['__logic__'] == 'alpha AND beta'
    conditions = doc['Alpha']['__filter__']['ModelY']['__conditional__']
    assert conditions[0]['condition_field'] == 'alpha'
    assert doc['Pipeline']['description'] == 'grouped'


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


def test_builder_write_accepts_string_path(tmp_path: Path) -> None:
    builder = SteeringBuilder()
    builder.add_processor('Solo')
    string_path = str(tmp_path / 'string-steering.toml')
    builder.write(string_path)
    assert Path(string_path).exists()


def test_builder_from_toml_text_parses() -> None:
    """Ensure TOML text parsing builds a usable builder."""

    text = """analysis_name = "Run"
processors_to_run = ["Proc"]

[Proc]
"""
    builder = SteeringBuilder.from_toml_text(text)
    assert builder.globals.analysis_name == 'Run'
    assert builder.globals.processors_to_run == ['Proc']


@pytest.mark.parametrize(
    'filename',
    ['autorad4-full.toml', 'mysql.toml', 'postgresql.toml', 'filter_exercise.toml'],
)
def test_autorad4_roundtrip_matches_load_steering_file(tmp_path: Path, datadir: Path, filename: str) -> None:
    """Round-tripping steering files through SteeringBuilder preserves every key."""

    steering_path = datadir / filename
    canonical = load_steering_file_legacy(steering_path)
    builder = SteeringBuilder.from_toml(steering_path)
    roundtrip_path = tmp_path / 'roundtrip.toml'
    builder.write(roundtrip_path)
    rebuilt = load_steering_file(roundtrip_path)
    assert canonical == rebuilt


def test_serializer_validation_error():
    """Cover serializer.py: raise issues[0] branch."""
    builder = SteeringBuilder()
    # An empty processors_to_run triggers a validation issue at SEMANTIC level
    builder.globals.processors_to_run = []
    with pytest.raises(mafw.mafw_errors.MissingProcessorsToRunIssue):
        serialize(builder, validation_level=ValidationLevel.SEMANTIC)


def test_serializer_validation_and_globals():
    """Cover serializer.py: validation and globals branches."""
    builder = SteeringBuilder()
    builder.add_processor('Proc')
    # Ensure validation works in serialize
    doc = serialize(builder, validation_level=ValidationLevel.SEMANTIC)
    assert doc is not None

    # Test globals with None values (should skip)
    builder.globals.analysis_name = None
    builder.globals.analysis_description = None
    builder.globals.new_only = None
    builder.globals.create_standard_tables = None
    doc = serialize(builder)
    assert 'analysis_name' not in doc
    assert 'analysis_description' not in doc
    assert 'new_only' not in doc
    assert 'create_standard_tables' not in doc


def test_serializer_filter_types_coverage():
    """Cover serializer.py: different filter types serialization."""
    builder = SteeringBuilder()
    builder.add_processor('Proc')

    # 1. FieldFilterConfig with logic
    ff1 = FieldFilterConfig(name='field_logic', model='Model', field_name='field_logic')
    ff1.conditions['c1'] = Condition('==', 1)
    ff1.logic = 'c1'

    # 2. FieldFilterConfig without logic
    ff2 = FieldFilterConfig(name='field_no_logic', model='Model', field_name='field_no_logic')
    ff2.conditions['c2'] = Condition('==', 2)

    builder.get_processor_config('Proc').filters['Model'] = [ff1, ff2]

    doc = serialize(builder)
    assert doc['Proc']['__filter__']['Model']['field_logic']['__logic__'] == 'c1'
    assert '__logic__' not in doc['Proc']['__filter__']['Model']['field_no_logic']


def test_serializer_conditional_branches():
    """Cover serializer.py: conditional filter branches."""
    builder = SteeringBuilder()
    builder.add_processor('Proc')

    # 1. Conditional with auto_named=False and all clauses
    cond1 = ConditionalFilterConfig(name='my_cond', model='Model', auto_named=False)
    cond1.condition = Condition('==', 1)
    cond1.condition_field = 'f1'
    cond1.then_clause = Condition('==', 2)
    cond1.then_field = 'f2'
    cond1.else_clause = Condition('==', 3)
    cond1.else_field = 'f3'

    # 2. Conditional without condition (auto_named=True default)
    cond2 = ConditionalFilterConfig(name='auto', model='Model', auto_named=True)
    cond2.then_clause = Condition('==', 4)
    cond2.then_field = 'f4'

    builder.get_processor_config('Proc').filters['Model'] = [cond1, cond2]
    doc = serialize(builder)
    res = doc['Proc']['__filter__']['Model']['__conditional__']
    assert res[0]['name'] == 'my_cond'
    assert res[0]['else_field'] == 'f3'
    assert 'name' not in res[1]
    assert 'condition_field' not in res[1]


def test_serializer_respects_enabled_flag():
    """Verify that disabled filters are serialized with __enable__ set to false."""
    builder = SteeringBuilder()
    builder.add_processor('Proc')

    # 1. Enabled Model Filter
    m1 = ModelFilterConfig(name='M1', model='M1', enabled=True)
    m1.conditions['c1'] = Condition('==', 1)

    # 2. Disabled Model Filter
    m2 = ModelFilterConfig(name='M2', model='M2', enabled=False)
    m2.conditions['c2'] = Condition('==', 2)

    # 3. Enabled Field Filter
    f1 = FieldFilterConfig(name='F1', model='M1', field_name='F1', enabled=True)
    f1.conditions['cf1'] = Condition('==', 3)

    # 4. Disabled Field Filter
    f2 = FieldFilterConfig(name='F2', model='M1', field_name='F2', enabled=False)
    f2.conditions['cf2'] = Condition('==', 4)

    # 5. Model with ONLY disabled filters
    f3 = FieldFilterConfig(name='F3', model='M3', field_name='F3', enabled=False)

    # 6. Enabled Conditional
    c1 = ConditionalFilterConfig(name='C1', model='M1', enabled=True)
    c1.then_field = 'tc1'
    c1.then_clause = Condition('==', 5)

    # 7. Disabled Conditional
    c2 = ConditionalFilterConfig(name='C2', model='M1', enabled=False)
    c2.then_field = 'tc2'
    c2.then_clause = Condition('==', 6)

    builder.get_processor_config('Proc').filters['M1'] = [m1, f1, f2, c1, c2]
    builder.get_processor_config('Proc').filters['M2'] = [m2]
    builder.get_processor_config('Proc').filters['M3'] = [f3]

    doc = serialize(builder)

    # M1 should be present with enabled filters and disabled entries preserved
    assert 'M1' in doc['Proc']['__filter__']
    assert 'c1' in doc['Proc']['__filter__']['M1']
    assert 'F1' in doc['Proc']['__filter__']['M1']
    assert doc['Proc']['__filter__']['M1']['F2']['__enable__'] is False
    assert len(doc['Proc']['__filter__']['M1']['__conditional__']) == 2
    assert doc['Proc']['__filter__']['M1']['__conditional__'][0]['then_field'] == 'tc1'
    assert doc['Proc']['__filter__']['M1']['__conditional__'][1]['__enable__'] is False

    # M2 should be present with disabled model filter
    assert doc['Proc']['__filter__']['M2']['__enable__'] is False

    # M3 should be present with disabled field filter
    assert doc['Proc']['__filter__']['M3']['F3']['__enable__'] is False


def test_serializer_deprecated_conditional_array():
    """Cover serializer.py: deprecated _build_conditional_array."""
    res = _build_conditional_array([{'a': 1}])
    assert len(res) == 1
    assert res[0]['a'] == 1


def test_normalize_logical_op_paths():
    """Cover various paths in _normalize_logical_op."""
    assert _normalize_logical_op(LogicalOp.EQ) == LogicalOp.EQ
    assert _normalize_logical_op(123) is None
    assert _normalize_logical_op('LogicalOp.GT') == LogicalOp.GT
    assert _normalize_logical_op('LogicalOp.INVALID') is None
    assert _normalize_logical_op('>') == LogicalOp.GT
    assert _normalize_logical_op('INVALID') is None


def test_serialize_condition_special_ops():
    """Cover IS_NULL and IS_NOT_NULL in _serialize_condition."""
    c1 = Condition(operator=LogicalOp.IS_NULL, value=None, is_implicit=False)
    assert _serialize_condition(c1) == {'op': 'LogicalOp.IS_NULL', 'value': True}

    c2 = Condition(operator=LogicalOp.IS_NOT_NULL, value='anything', is_implicit=False)
    assert _serialize_condition(c2) == {'op': 'LogicalOp.IS_NOT_NULL', 'value': True}


def test_serialize_with_validation():
    """Cover validation_level in serialize."""
    builder = SteeringBuilder()
    builder.add_processor('test')
    serialize(builder, validation_level=ValidationLevel.SEMANTIC)


def test_add_processor_table_no_filter_root():
    """Cover has_filter_root=False in _add_processor_table."""
    builder = SteeringBuilder()
    pc = ProcessorConfig(name='test')
    pc.filter_logic = 'a AND b'
    pc.has_filter_root = False
    builder.processors['test'] = pc
    # Trigger serialization of this processor
    builder.globals.processors_to_run = ['test']
    serialize(builder)


def test_serialize_model_filter_with_logic():
    """Cover line 162 in serializer.py."""
    builder = SteeringBuilder()
    mfc = ModelFilterConfig(name='Model', model='Model')
    mfc.logic = 'c1'
    mfc.conditions['c1'] = Condition(operator='==', value=1)
    builder.processors['test'] = ProcessorConfig(name='test', filters={'Model': [mfc]})
    builder.add_processor('test')
    serialize(builder)
