/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2024 KiCad Developers, see AUTHORS.txt for contributors.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, you may find one here:
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * or you may search the http://www.gnu.org website for the version 2 license,
 * or you may write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 */

#include <boost/test/unit_test.hpp>
#include <wx/ffile.h>
#include <core/typeinfo.h>
#include <drc/rule_editor/drc_rule_editor_utils.h>
#include <drc/rule_editor/drc_re_via_style_constraint_data.h>
#include <drc/rule_editor/drc_re_routing_width_constraint_data.h>
#include <drc/rule_editor/drc_re_rtg_diff_pair_constraint_data.h>
#include <drc/rule_editor/drc_re_abs_length_two_constraint_data.h>
#include <drc/rule_editor/drc_re_custom_rule_constraint_data.h>
#include <drc/rule_editor/drc_re_permitted_layers_constraint_data.h>
#include <drc/rule_editor/drc_re_min_txt_ht_th_constraint_data.h>
#include <drc/rule_editor/drc_re_allowed_orientation_constraint_data.h>
#include <drc/rule_editor/drc_rule_editor_enums.h>
#include <drc/rule_editor/drc_re_numeric_constraint_types.h> 
#include <drc/drc_rule.h>
#include <drc/rule_editor/drc_re_base_constraint_data.h>
#include <drc/rule_editor/drc_re_numeric_input_constraint_data.h>
#include <drc/rule_editor/drc_re_bool_input_constraint_data.h>
#include <drc/rule_editor/drc_re_vias_under_smd_constraint_data.h>
#include <drc/rule_editor/drc_re_panel_matcher.h>
#include <drc/rule_editor/drc_re_rule_loader.h>
#include <drc/rule_editor/drc_re_rule_saver.h>
#include <dialogs/rule_editor_dialog_base.h>

BOOST_AUTO_TEST_SUITE( DRC_RULE_EDITOR )

BOOST_AUTO_TEST_CASE( RoundTripViaStyle )
{
    DRC_RE_VIA_STYLE_CONSTRAINT_DATA original( 0, 0, "My_Via_Rule", 0.5, 0.8, 0.2, 0.4 );
    original.SetConstraintCode( "via_style" );
    original.SetRuleCondition( "A.NetClass == 'Power'" );

    RULE_GENERATION_CONTEXT ctx;
    ctx.ruleName = original.GetRuleName();
    ctx.conditionExpression = original.GetRuleCondition();
    ctx.constraintCode = original.GetConstraintCode();

    wxString ruleText = original.GenerateRule( ctx );

    auto parsedRules = DRC_RULE_EDITOR_UTILS::ParseRules( ruleText );

    BOOST_REQUIRE_EQUAL( parsedRules.size(), 1 );
    auto parsed = std::dynamic_pointer_cast<DRC_RE_VIA_STYLE_CONSTRAINT_DATA>( parsedRules[0] );
    BOOST_REQUIRE( parsed );

    BOOST_CHECK_EQUAL( parsed->GetRuleName(), original.GetRuleName() );
    BOOST_CHECK_EQUAL( parsed->GetRuleCondition(), original.GetRuleCondition() );
    BOOST_CHECK_CLOSE( parsed->GetMinViaDiameter(), original.GetMinViaDiameter(), 0.0001 );
    BOOST_CHECK_CLOSE( parsed->GetMaxViaDiameter(), original.GetMaxViaDiameter(), 0.0001 );
    BOOST_CHECK_CLOSE( parsed->GetMinViaHoleSize(), original.GetMinViaHoleSize(), 0.0001 );
    BOOST_CHECK_CLOSE( parsed->GetMaxViaHoleSize(), original.GetMaxViaHoleSize(), 0.0001 );
}

BOOST_AUTO_TEST_CASE( RoundTripRoutingWidth )
{
    DRC_RE_ROUTING_WIDTH_CONSTRAINT_DATA original( 0, 0, "My_Track_Rule", 0.3, 0.1 );
    original.SetConstraintCode( "track_width" );
    original.SetRuleCondition( "A.NetClass == 'Signal'" );

    RULE_GENERATION_CONTEXT ctx;
    ctx.ruleName = original.GetRuleName();
    ctx.conditionExpression = original.GetRuleCondition();
    ctx.constraintCode = original.GetConstraintCode();

    wxString ruleText = original.GenerateRule( ctx );

    auto parsedRules = DRC_RULE_EDITOR_UTILS::ParseRules( ruleText );

    BOOST_REQUIRE_EQUAL( parsedRules.size(), 1 );
    auto parsed = std::dynamic_pointer_cast<DRC_RE_ROUTING_WIDTH_CONSTRAINT_DATA>( parsedRules[0] );
    BOOST_REQUIRE( parsed );

    BOOST_CHECK_EQUAL( parsed->GetRuleName(), original.GetRuleName() );
    BOOST_CHECK_EQUAL( parsed->GetRuleCondition(), original.GetRuleCondition() );
    BOOST_CHECK_CLOSE( parsed->GetOptWidth(), original.GetOptWidth(), 0.0001 );
    BOOST_CHECK_CLOSE( parsed->GetWidthTolerance(), original.GetWidthTolerance(), 0.0001 );
}

BOOST_AUTO_TEST_CASE( SaveRules )
{
    std::vector<std::shared_ptr<DRC_RE_BASE_CONSTRAINT_DATA>> rules;

    auto rule1 = std::make_shared<DRC_RE_VIA_STYLE_CONSTRAINT_DATA>( 0, 0, "ViaRule", 0.5, 0.8, 0.2, 0.4 );
    rule1->SetConstraintCode( "via_style" );
    rule1->SetRuleCondition( "A.NetClass == 'Power'" );
    rules.push_back( rule1 );

    auto rule2 = std::make_shared<DRC_RE_ROUTING_WIDTH_CONSTRAINT_DATA>( 0, 0, "TrackRule", 0.3, 0.1 );
    rule2->SetConstraintCode( "track_width" );
    rule2->SetRuleCondition( "A.NetClass == 'Signal'" );
    rules.push_back( rule2 );

    wxString filename = "test_rules.dru";

    // We pass nullptr for board, so layers won't be saved, which is fine for this test
    bool result = DRC_RULE_EDITOR_UTILS::SaveRules( filename, rules, nullptr );
    BOOST_REQUIRE( result );

    wxFFile file( filename );
    BOOST_REQUIRE( file.IsOpened() );
    wxString content;
    file.ReadAll( &content );

    // Verify content
    BOOST_CHECK( content.Contains( "(version 1)" ) );
    BOOST_CHECK( content.Contains( "(rule ViaRule" ) );
    BOOST_CHECK( content.Contains( "(constraint via_diameter" ) );
    BOOST_CHECK( content.Contains( "(constraint hole_size" ) );
    BOOST_CHECK( content.Contains( "(rule TrackRule" ) );
    BOOST_CHECK( content.Contains( "(constraint track_width" ) );

    // Clean up
    wxRemoveFile( filename );
}

BOOST_AUTO_TEST_CASE( SaveRulesToFileLogic )
{
    // Mimic m_ruleTreeNodeDatas
    std::vector<RULE_TREE_NODE> ruleTreeNodeDatas;

    // Add a rule node
    RULE_TREE_NODE node;
    node.m_nodeId = 1;
    node.m_nodeName = "TestRule";
    node.m_nodeType = RULE; // 4

    auto data = std::make_shared<DRC_RE_BASE_CONSTRAINT_DATA>( 1, 0, "TestRule" );
    data->SetConstraintCode( "clearance" );
    node.m_nodeData = data;

    ruleTreeNodeDatas.push_back( node );

    // Mimic SaveRulesToFile logic
    std::vector<std::shared_ptr<DRC_RE_BASE_CONSTRAINT_DATA>> rules;

    for( const RULE_TREE_NODE& n : ruleTreeNodeDatas )
    {
        if( n.m_nodeType != RULE )
            continue;

        auto d = std::dynamic_pointer_cast<DRC_RE_BASE_CONSTRAINT_DATA>( n.m_nodeData );

        if( d )
            rules.push_back( d );
    }

    BOOST_CHECK_EQUAL( rules.size(), 1 );
}

BOOST_AUTO_TEST_CASE( SaveNumericRules )
{
    auto rule = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>( 0, 0, 0.2, "ClearanceRule" );
    rule->SetConstraintCode( "clearance" );

    RULE_GENERATION_CONTEXT ctx;
    ctx.ruleName = rule->GetRuleName();
    ctx.constraintCode = rule->GetConstraintCode();

    wxString ruleText = rule->GenerateRule( ctx );

    BOOST_CHECK( ruleText.Contains( "(constraint clearance (min 0.2mm))" ) );

    // Test via_count (max, unitless)
    auto rule2 = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>( 0, 0, 10, "ViaCountRule" );
    rule2->SetConstraintCode( "via_count" );
    ctx.ruleName = rule2->GetRuleName();
    ctx.constraintCode = rule2->GetConstraintCode();

    ruleText = rule2->GenerateRule( ctx );
    BOOST_CHECK( ruleText.Contains( "(constraint via_count (max 10))" ) );

    // Test track_angle (min, deg)
    auto rule3 = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>( 0, 0, 45, "AngleRule" );
    rule3->SetConstraintCode( "track_angle" );
    ctx.ruleName = rule3->GetRuleName();
    ctx.constraintCode = rule3->GetConstraintCode();

    ruleText = rule3->GenerateRule( ctx );
    BOOST_CHECK( ruleText.Contains( "(constraint track_angle (min 45deg))" ) );
}

BOOST_AUTO_TEST_CASE( SaveBoolRules )
{
    auto rule = std::make_shared<DRC_RE_BOOL_INPUT_CONSTRAINT_DATA>( 0, 0, true, "DisallowTestRule" );
    rule->SetConstraintCode( "disallow" );

    RULE_GENERATION_CONTEXT ctx;
    ctx.ruleName = rule->GetRuleName();
    ctx.constraintCode = rule->GetConstraintCode();

    wxString ruleText = rule->GenerateRule( ctx );

    BOOST_CHECK( ruleText.Contains( "(constraint disallow)" ) );

    // Test false value (should not generate constraint)
    auto rule2 = std::make_shared<DRC_RE_BOOL_INPUT_CONSTRAINT_DATA>( 0, 0, false, "NoDisallowTestRule" );
    rule2->SetConstraintCode( "disallow" );
    ctx.ruleName = rule2->GetRuleName();
    ctx.constraintCode = rule2->GetConstraintCode();

    ruleText = rule2->GenerateRule( ctx );
    BOOST_CHECK( ruleText.IsEmpty() );
}

BOOST_AUTO_TEST_CASE( ParseRulesCategories )
{
    wxString rules =
        "(version 1)\n"
        "(rule \"Clearance Rule\"\n"
        "\t(constraint clearance (min 0.2mm))\n"
        "\t(condition \"A.NetClass == 'Power'\"))\n"
        "(rule \"Via Style Rule\"\n"
        "\t(constraint via_diameter (min 0.5mm))\n"
        "\t(constraint hole_size (min 0.3mm))\n"
        "\t(condition \"A.NetClass == 'Power'\"))\n";

    auto parsedRules = DRC_RULE_EDITOR_UTILS::ParseRules( rules );

    BOOST_REQUIRE_EQUAL( parsedRules.size(), 2 );

    // Check Clearance Rule
    auto clearanceRule = parsedRules[0];
    BOOST_CHECK_EQUAL( clearanceRule->GetRuleName(), "Clearance Rule" );
    BOOST_CHECK_EQUAL( clearanceRule->GetConstraintCode(), "clearance" );

    // Verify it is a numeric constraint data
    auto numericData = std::dynamic_pointer_cast<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>( clearanceRule );
    BOOST_REQUIRE( numericData );
    BOOST_CHECK_CLOSE( numericData->GetNumericInputValue(), 0.2, 0.0001 );

    // Check Via Style Rule
    auto viaStyleRule = parsedRules[1];
    BOOST_CHECK_EQUAL( viaStyleRule->GetRuleName(), "Via Style Rule" );
    BOOST_CHECK_EQUAL( viaStyleRule->GetConstraintCode(), "via_style" );

    auto viaData = std::dynamic_pointer_cast<DRC_RE_VIA_STYLE_CONSTRAINT_DATA>( viaStyleRule );
    BOOST_REQUIRE( viaData );
    BOOST_CHECK_CLOSE( viaData->GetMinViaDiameter(), 0.5, 0.0001 );
    BOOST_CHECK_CLOSE( viaData->GetMinViaHoleSize(), 0.3, 0.0001 );
}

BOOST_AUTO_TEST_CASE( FactoryRegistration )
{
    // Register a custom parser for 'clearance' to override default
    bool parserCalled = false;
    DRC_RULE_EDITOR_UTILS::RegisterRuleConverter(
        [&]( const std::shared_ptr<DRC_RULE>& aRule ) -> std::shared_ptr<DRC_RE_BASE_CONSTRAINT_DATA>
        {
            if( aRule->FindConstraint( CLEARANCE_CONSTRAINT ) )
            {
                parserCalled = true;
                auto data = std::make_shared<DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA>( 0, 0, aRule->m_Name );
                data->SetConstraintCode( "custom_clearance" );
                return data;
            }
            return nullptr;
        } );

    wxString ruleText = "(version 1) (rule \"Test Rule\" (constraint clearance (min 1.0mm)) (condition \"A.Type == 'Pad'\"))";
    auto parsedRules = DRC_RULE_EDITOR_UTILS::ParseRules( ruleText );

    BOOST_CHECK( parserCalled );
    BOOST_REQUIRE_EQUAL( parsedRules.size(), 1 );
    BOOST_CHECK_EQUAL( parsedRules[0]->GetConstraintCode(), "custom_clearance" );
}

BOOST_AUTO_TEST_CASE( ValidateViaStyleValid )
{
    DRC_RE_VIA_STYLE_CONSTRAINT_DATA data( 0, 0, "ValidRule", 0.5, 0.8, 0.2, 0.4 );

    VALIDATION_RESULT result = data.Validate();

    BOOST_CHECK( result.isValid );
    BOOST_CHECK( result.errors.empty() );
}

BOOST_AUTO_TEST_CASE( ValidateViaStyleInvalidMinGreaterThanMax )
{
    // min > max for via diameter
    DRC_RE_VIA_STYLE_CONSTRAINT_DATA data( 0, 0, "InvalidRule", 0.9, 0.5, 0.2, 0.4 );

    VALIDATION_RESULT result = data.Validate();

    BOOST_CHECK( !result.isValid );
    BOOST_CHECK( !result.errors.empty() );

    // Should have error about min > max
    bool foundMinMaxError = false;
    for( const auto& error : result.errors )
    {
        if( error.find( "Minimum Via Diameter cannot be greater than Maximum Via Diameter" ) != std::string::npos )
            foundMinMaxError = true;
    }
    BOOST_CHECK( foundMinMaxError );
}

BOOST_AUTO_TEST_CASE( ValidateViaStyleInvalidNegativeValues )
{
    // Negative values
    DRC_RE_VIA_STYLE_CONSTRAINT_DATA data( 0, 0, "NegativeRule", -0.5, 0.8, 0.2, 0.4 );

    VALIDATION_RESULT result = data.Validate();

    BOOST_CHECK( !result.isValid );
    BOOST_CHECK( !result.errors.empty() );

    // Should have error about negative value
    bool foundNegativeError = false;
    for( const auto& error : result.errors )
    {
        if( error.find( "must be greater than 0" ) != std::string::npos )
            foundNegativeError = true;
    }
    BOOST_CHECK( foundNegativeError );
}

BOOST_AUTO_TEST_CASE( FactoryOverwrite )
{
    // Register a parser
    DRC_RULE_EDITOR_UTILS::RegisterRuleConverter(
        []( const std::shared_ptr<DRC_RULE>& aRule ) -> std::shared_ptr<DRC_RE_BASE_CONSTRAINT_DATA> {
            if( aRule->m_Name == "Test" )
                return std::make_shared<DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA>( 0, 0, aRule->m_Name + "_1" );
            return nullptr;
        } );

    // Overwrite it (prepend)
    DRC_RULE_EDITOR_UTILS::RegisterRuleConverter(
        []( const std::shared_ptr<DRC_RULE>& aRule ) -> std::shared_ptr<DRC_RE_BASE_CONSTRAINT_DATA> {
            if( aRule->m_Name == "Test" )
                return std::make_shared<DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA>( 0, 0, aRule->m_Name + "_2" );
            return nullptr;
        } );

    wxString ruleText = "(version 1) (rule \"Test\" (constraint clearance (min 1.0mm)))";
    auto parsedRules = DRC_RULE_EDITOR_UTILS::ParseRules( ruleText );

    BOOST_REQUIRE_EQUAL( parsedRules.size(), 1 );
    BOOST_CHECK_EQUAL( parsedRules[0]->GetRuleName(), "Test_2" );
}

BOOST_AUTO_TEST_CASE( ValidateAbsLengthTwoValid )
{
    // Valid: min < opt < max, all positive
    DRC_RE_ABSOLUTE_LENGTH_TWO_CONSTRAINT_DATA data( 0, 0, 3.0, 2.0, "ValidLengthRule" );

    VALIDATION_RESULT result = data.Validate();

    BOOST_CHECK( result.isValid );
    BOOST_CHECK( result.errors.empty() );
}

BOOST_AUTO_TEST_CASE( ValidateAbsLengthTwoInvalid )
{
    // Invalid: tolerance too large, resulting minimum length is not positive
    DRC_RE_ABSOLUTE_LENGTH_TWO_CONSTRAINT_DATA data1( 0, 0, 2.0, 5.0, "InvalidToleranceRule" );

    VALIDATION_RESULT result1 = data1.Validate();

    BOOST_CHECK( !result1.isValid );
    BOOST_CHECK( !result1.errors.empty() );

    bool foundToleranceError = false;

    for( const auto& error : result1.errors )
    {
        if( error.find( "Tolerance is too large" ) != std::string::npos )
            foundToleranceError = true;
    }

    BOOST_CHECK( foundToleranceError );

    // Invalid: negative tolerance
    DRC_RE_ABSOLUTE_LENGTH_TWO_CONSTRAINT_DATA data2( 0, 0, 3.0, -1.0, "NegativeToleranceRule" );

    VALIDATION_RESULT result2 = data2.Validate();

    BOOST_CHECK( !result2.isValid );

    bool foundNegativeError = false;

    for( const auto& error : result2.errors )
    {
        if( error.find( "Tolerance must be greater than or equal to 0" ) != std::string::npos )
            foundNegativeError = true;
    }

    BOOST_CHECK( foundNegativeError );
}

BOOST_AUTO_TEST_CASE( ValidateDiffPairValid )
{
    // Valid: all positive, min <= preferred <= max for width and gap
    // Constructor: id, parentId, ruleName, optWidth, widthTolerance, optGap, gapTolerance, maxUncoupledLength, maxSkew
    DRC_RE_ROUTING_DIFF_PAIR_CONSTRAINT_DATA data( 0, 0, "ValidDiffPairRule", 0.3, 0.1, 0.15, 0.05, 10.0 );

    VALIDATION_RESULT result = data.Validate();

    BOOST_CHECK( result.isValid );
    BOOST_CHECK( result.errors.empty() );
}

BOOST_AUTO_TEST_CASE( ValidateDiffPairInvalid )
{
    // Invalid: min width > max width
    DRC_RE_ROUTING_DIFF_PAIR_CONSTRAINT_DATA data1( 0, 0, "InvalidWidthRule", 0.3, 0.5, 0.15, 0.05, 10.0 );

    VALIDATION_RESULT result1 = data1.Validate();

    BOOST_CHECK( !result1.isValid );
    BOOST_CHECK( !result1.errors.empty() );

    bool foundWidthError = false;
    for( const auto& error : result1.errors )
    {
        if( error.find( "Width Tolerance must be less than Optimum Width" ) != std::string::npos )
            foundWidthError = true;
    }
    BOOST_CHECK( foundWidthError );

    // Invalid: negative gap
    DRC_RE_ROUTING_DIFF_PAIR_CONSTRAINT_DATA data2( 0, 0, "NegativeGapRule", 0.3, 0.1, 0.15, -0.1, 10.0 );

    VALIDATION_RESULT result2 = data2.Validate();

    BOOST_CHECK( !result2.isValid );

    bool foundNegativeError = false;
    for( const auto& error : result2.errors )
    {
        if( error.find( "must be greater than or equal to 0" ) != std::string::npos )
            foundNegativeError = true;
    }
    BOOST_CHECK( foundNegativeError );
}

BOOST_AUTO_TEST_CASE( ValidatePermittedLayersValid )
{
    // Valid: at least one layer is selected (top layer enabled)
    DRC_RE_PERMITTED_LAYERS_CONSTRAINT_DATA data1( 0, 0, "ValidTopLayerRule", true, false );

    VALIDATION_RESULT result1 = data1.Validate();

    BOOST_CHECK( result1.isValid );
    BOOST_CHECK( result1.errors.empty() );

    // Valid: at least one layer is selected (bottom layer enabled)
    DRC_RE_PERMITTED_LAYERS_CONSTRAINT_DATA data2( 0, 0, "ValidBottomLayerRule", false, true );

    VALIDATION_RESULT result2 = data2.Validate();

    BOOST_CHECK( result2.isValid );
    BOOST_CHECK( result2.errors.empty() );

    // Valid: both layers enabled
    DRC_RE_PERMITTED_LAYERS_CONSTRAINT_DATA data3( 0, 0, "ValidBothLayersRule", true, true );

    VALIDATION_RESULT result3 = data3.Validate();

    BOOST_CHECK( result3.isValid );
    BOOST_CHECK( result3.errors.empty() );
}

BOOST_AUTO_TEST_CASE( ValidatePermittedLayersInvalid )
{
    // Invalid: no layers selected
    DRC_RE_PERMITTED_LAYERS_CONSTRAINT_DATA data( 0, 0, "NoLayersRule", false, false );

    VALIDATION_RESULT result = data.Validate();

    BOOST_CHECK( !result.isValid );
    BOOST_CHECK( !result.errors.empty() );

    bool foundLayerError = false;
    for( const auto& error : result.errors )
    {
        if( error.find( "At least one layer must be selected" ) != std::string::npos )
            foundLayerError = true;
    }
    BOOST_CHECK( foundLayerError );
}

BOOST_AUTO_TEST_CASE( ValidateNumericInputValid )
{
    DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA data( 0, 0, 0.5, "ValidRule" );

    VALIDATION_RESULT result = data.Validate();

    BOOST_CHECK( result.isValid );
    BOOST_CHECK( result.errors.empty() );
}

BOOST_AUTO_TEST_CASE( ValidateNumericInputInvalidNegative )
{
    DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA data( 0, 0, -0.5, "InvalidRule" );

    VALIDATION_RESULT result = data.Validate();

    BOOST_CHECK( !result.isValid );
    BOOST_CHECK( !result.errors.empty() );

    // Should have error about value not being greater than 0
    bool foundError = false;
    for( const auto& error : result.errors )
    {
        if( error.find( "must be greater than 0" ) != std::string::npos )
            foundError = true;
    }
    BOOST_CHECK( foundError );
}

BOOST_AUTO_TEST_CASE( ValidateRoutingWidthValid )
{
    DRC_RE_ROUTING_WIDTH_CONSTRAINT_DATA data( 0, 0, "ValidRule", 0.3, 0.1 );

    VALIDATION_RESULT result = data.Validate();

    BOOST_CHECK( result.isValid );
    BOOST_CHECK( result.errors.empty() );
}

BOOST_AUTO_TEST_CASE( ValidateRoutingWidthInvalidToleranceTooLarge )
{
    // tolerance >= opt width
    DRC_RE_ROUTING_WIDTH_CONSTRAINT_DATA data( 0, 0, "InvalidRule", 0.3, 0.5 );

    VALIDATION_RESULT result = data.Validate();

    BOOST_CHECK( !result.isValid );
    BOOST_CHECK( !result.errors.empty() );

    bool foundError = false;
    for( const auto& error : result.errors )
    {
        if( error.find( "Width Tolerance must be less than Optimum Width" ) != std::string::npos )
            foundError = true;
    }
    BOOST_CHECK( foundError );
}

BOOST_AUTO_TEST_CASE( ValidateAllowedOrientationValid )
{
    // Valid: at least one orientation is selected (0 degrees)
    DRC_RE_ALLOWED_ORIENTATION_CONSTRAINT_DATA data1( 0, 0, true, false, false, false, false, "ValidZeroDegRule" );

    VALIDATION_RESULT result1 = data1.Validate();

    BOOST_CHECK( result1.isValid );
    BOOST_CHECK( result1.errors.empty() );

    // Valid: 90 degrees selected
    DRC_RE_ALLOWED_ORIENTATION_CONSTRAINT_DATA data2( 0, 0, false, true, false, false, false, "ValidNinetyDegRule" );

    VALIDATION_RESULT result2 = data2.Validate();

    BOOST_CHECK( result2.isValid );
    BOOST_CHECK( result2.errors.empty() );

    // Valid: all degrees selected
    DRC_RE_ALLOWED_ORIENTATION_CONSTRAINT_DATA data3( 0, 0, false, false, false, false, true, "ValidAllDegreesRule" );

    VALIDATION_RESULT result3 = data3.Validate();

    BOOST_CHECK( result3.isValid );
    BOOST_CHECK( result3.errors.empty() );

    // Valid: multiple orientations selected
    DRC_RE_ALLOWED_ORIENTATION_CONSTRAINT_DATA data4( 0, 0, true, true, false, false, false, "ValidMultipleRule" );

    VALIDATION_RESULT result4 = data4.Validate();

    BOOST_CHECK( result4.isValid );
    BOOST_CHECK( result4.errors.empty() );
}

BOOST_AUTO_TEST_CASE( ValidateAllowedOrientationInvalid )
{
    // Invalid: no orientation selected
    DRC_RE_ALLOWED_ORIENTATION_CONSTRAINT_DATA data( 0, 0, false, false, false, false, false, "NoOrientationRule" );

    VALIDATION_RESULT result = data.Validate();

    BOOST_CHECK( !result.isValid );
    BOOST_CHECK( !result.errors.empty() );

    bool foundOrientationError = false;
    for( const auto& error : result.errors )
    {
        if( error.find( "At least one orientation must be selected" ) != std::string::npos )
            foundOrientationError = true;
    }
    BOOST_CHECK( foundOrientationError );
}

BOOST_AUTO_TEST_CASE( ValidateBoolInputValid )
{
    // Test with true value
    DRC_RE_BOOL_INPUT_CONSTRAINT_DATA dataTrue( 0, 0, true, "BoolRuleTrue" );

    VALIDATION_RESULT resultTrue = dataTrue.Validate();

    BOOST_CHECK( resultTrue.isValid );
    BOOST_CHECK( resultTrue.errors.empty() );

    // Test with false value
    DRC_RE_BOOL_INPUT_CONSTRAINT_DATA dataFalse( 0, 0, false, "BoolRuleFalse" );

    VALIDATION_RESULT resultFalse = dataFalse.Validate();

    BOOST_CHECK( resultFalse.isValid );
    BOOST_CHECK( resultFalse.errors.empty() );
}

BOOST_AUTO_TEST_CASE( ValidateMinTxtHtThValid )
{
    // Valid: both text height and thickness are positive
    DRC_RE_MINIMUM_TEXT_HEIGHT_THICKNESS_CONSTRAINT_DATA data( 0, 0, "ValidTextRule", 1.0, 0.15 );

    VALIDATION_RESULT result = data.Validate();

    BOOST_CHECK( result.isValid );
    BOOST_CHECK( result.errors.empty() );
}

BOOST_AUTO_TEST_CASE( ValidateMinTxtHtThInvalid )
{
    // Invalid: negative text height
    DRC_RE_MINIMUM_TEXT_HEIGHT_THICKNESS_CONSTRAINT_DATA data1( 0, 0, "NegativeHeightRule", -1.0, 0.15 );

    VALIDATION_RESULT result1 = data1.Validate();

    BOOST_CHECK( !result1.isValid );
    BOOST_CHECK( !result1.errors.empty() );

    bool foundHeightError = false;
    for( const auto& error : result1.errors )
    {
        if( error.find( "Minimum Text Height must be greater than 0" ) != std::string::npos )
            foundHeightError = true;
    }
    BOOST_CHECK( foundHeightError );

    // Invalid: negative text thickness
    DRC_RE_MINIMUM_TEXT_HEIGHT_THICKNESS_CONSTRAINT_DATA data2( 0, 0, "NegativeThicknessRule", 1.0, -0.15 );

    VALIDATION_RESULT result2 = data2.Validate();

    BOOST_CHECK( !result2.isValid );

    bool foundThicknessError = false;
    for( const auto& error : result2.errors )
    {
        if( error.find( "Minimum Text Thickness must be greater than 0" ) != std::string::npos )
            foundThicknessError = true;
    }
    BOOST_CHECK( foundThicknessError );

    // Invalid: both zero (default values)
    DRC_RE_MINIMUM_TEXT_HEIGHT_THICKNESS_CONSTRAINT_DATA data3;

    VALIDATION_RESULT result3 = data3.Validate();

    BOOST_CHECK( !result3.isValid );
    BOOST_CHECK_EQUAL( result3.errors.size(), 2 );
}

BOOST_AUTO_TEST_CASE( ValidateCustomRuleValid )
{
    DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA data( 0, 0, "ValidCustomRule" );
    data.SetRuleText( "(constraint clearance (min 0.2mm))" );

    VALIDATION_RESULT result = data.Validate();

    BOOST_CHECK( result.isValid );
    BOOST_CHECK( result.errors.empty() );
}

BOOST_AUTO_TEST_CASE( ValidateCustomRuleInvalid )
{
    DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA data( 0, 0, "InvalidCustomRule" );
    data.SetRuleText( "" );

    VALIDATION_RESULT result = data.Validate();

    BOOST_CHECK( !result.isValid );
    BOOST_CHECK( !result.errors.empty() );

    // Should have error about empty rule text
    bool foundEmptyError = false;
    for( const auto& error : result.errors )
    {
        if( error.find( "Rule text cannot be empty" ) != std::string::npos )
            foundEmptyError = true;
    }
    BOOST_CHECK( foundEmptyError );
}


// ============================================================================
// Panel Matcher Tests (Phase 5.1)
// ============================================================================

BOOST_AUTO_TEST_CASE( PanelMatcherExactMatchViaStyle )
{
    // Test: Rule with via_diameter + hole_size → VIA_STYLE panel
    DRC_RULE rule( "ViaStyleRule" );

    DRC_CONSTRAINT viaDia( VIA_DIAMETER_CONSTRAINT );
    viaDia.Value().SetMin( 500000 );
    viaDia.Value().SetOpt( 600000 );
    viaDia.Value().SetMax( 800000 );
    rule.AddConstraint( viaDia );

    DRC_CONSTRAINT holeSize( HOLE_SIZE_CONSTRAINT );
    holeSize.Value().SetMin( 200000 );
    holeSize.Value().SetOpt( 300000 );
    holeSize.Value().SetMax( 400000 );
    rule.AddConstraint( holeSize );

    DRC_PANEL_MATCHER matcher;
    std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );

    BOOST_REQUIRE_EQUAL( matches.size(), 1 );
    BOOST_CHECK_EQUAL( matches[0].panelType, VIA_STYLE );
    BOOST_CHECK_EQUAL( matches[0].claimedConstraints.size(), 2 );
    BOOST_CHECK( matches[0].claimedConstraints.count( VIA_DIAMETER_CONSTRAINT ) > 0 );
    BOOST_CHECK( matches[0].claimedConstraints.count( HOLE_SIZE_CONSTRAINT ) > 0 );
}

BOOST_AUTO_TEST_CASE( PanelMatcherPartialMatchViaDiameter )
{
    // Test: Rule with only via_diameter → MINIMUM_VIA_DIAMETER panel
    DRC_RULE rule( "ViaDiameterOnlyRule" );

    DRC_CONSTRAINT viaDia( VIA_DIAMETER_CONSTRAINT );
    viaDia.Value().SetMin( 500000 );
    rule.AddConstraint( viaDia );

    DRC_PANEL_MATCHER matcher;
    std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );

    BOOST_REQUIRE_EQUAL( matches.size(), 1 );
    BOOST_CHECK_EQUAL( matches[0].panelType, MINIMUM_VIA_DIAMETER );
    BOOST_CHECK_EQUAL( matches[0].claimedConstraints.size(), 1 );
    BOOST_CHECK( matches[0].claimedConstraints.count( VIA_DIAMETER_CONSTRAINT ) > 0 );
}

BOOST_AUTO_TEST_CASE( PanelMatcherSplitRule )
{
    // Test: Rule with via_diameter + hole_size + clearance → 2 panels
    DRC_RULE rule( "SplitRule" );

    DRC_CONSTRAINT viaDia( VIA_DIAMETER_CONSTRAINT );
    viaDia.Value().SetMin( 500000 );
    rule.AddConstraint( viaDia );

    DRC_CONSTRAINT holeSize( HOLE_SIZE_CONSTRAINT );
    holeSize.Value().SetMin( 200000 );
    rule.AddConstraint( holeSize );

    DRC_CONSTRAINT clearance( CLEARANCE_CONSTRAINT );
    clearance.Value().SetMin( 150000 );
    rule.AddConstraint( clearance );

    DRC_PANEL_MATCHER matcher;
    std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );

    BOOST_REQUIRE_EQUAL( matches.size(), 2 );

    // VIA_STYLE should claim via_diameter + hole_size
    BOOST_CHECK_EQUAL( matches[0].panelType, VIA_STYLE );
    BOOST_CHECK_EQUAL( matches[0].claimedConstraints.size(), 2 );

    // MINIMUM_CLEARANCE should claim clearance
    BOOST_CHECK_EQUAL( matches[1].panelType, MINIMUM_CLEARANCE );
    BOOST_CHECK_EQUAL( matches[1].claimedConstraints.size(), 1 );
    BOOST_CHECK( matches[1].claimedConstraints.count( CLEARANCE_CONSTRAINT ) > 0 );
}

BOOST_AUTO_TEST_CASE( PanelMatcherPriorityDiffPairOverRoutingWidth )
{
    // Test: Rule with track_width + diff_pair_gap → ROUTING_DIFF_PAIR (not ROUTING_WIDTH)
    DRC_RULE rule( "DiffPairRule" );

    DRC_CONSTRAINT trackWidth( TRACK_WIDTH_CONSTRAINT );
    trackWidth.Value().SetMin( 200000 );
    trackWidth.Value().SetOpt( 250000 );
    trackWidth.Value().SetMax( 300000 );
    rule.AddConstraint( trackWidth );

    DRC_CONSTRAINT diffGap( DIFF_PAIR_GAP_CONSTRAINT );
    diffGap.Value().SetMin( 100000 );
    diffGap.Value().SetOpt( 150000 );
    diffGap.Value().SetMax( 200000 );
    rule.AddConstraint( diffGap );

    DRC_PANEL_MATCHER matcher;
    std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );

    BOOST_REQUIRE_EQUAL( matches.size(), 1 );
    BOOST_CHECK_EQUAL( matches[0].panelType, ROUTING_DIFF_PAIR );
    BOOST_CHECK( matches[0].claimedConstraints.count( TRACK_WIDTH_CONSTRAINT ) > 0 );
    BOOST_CHECK( matches[0].claimedConstraints.count( DIFF_PAIR_GAP_CONSTRAINT ) > 0 );
}

BOOST_AUTO_TEST_CASE( PanelMatcherDiffPairWithOptionalUncoupled )
{
    // Test: Rule with track_width + diff_pair_gap + uncoupled → ROUTING_DIFF_PAIR claims all three
    DRC_RULE rule( "DiffPairWithUncoupledRule" );

    DRC_CONSTRAINT trackWidth( TRACK_WIDTH_CONSTRAINT );
    trackWidth.Value().SetMin( 200000 );
    rule.AddConstraint( trackWidth );

    DRC_CONSTRAINT diffGap( DIFF_PAIR_GAP_CONSTRAINT );
    diffGap.Value().SetMin( 100000 );
    rule.AddConstraint( diffGap );

    DRC_CONSTRAINT uncoupled( MAX_UNCOUPLED_CONSTRAINT );
    uncoupled.Value().SetMax( 5000000 );
    rule.AddConstraint( uncoupled );

    DRC_PANEL_MATCHER matcher;
    std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );

    BOOST_REQUIRE_EQUAL( matches.size(), 1 );
    BOOST_CHECK_EQUAL( matches[0].panelType, ROUTING_DIFF_PAIR );
    BOOST_CHECK_EQUAL( matches[0].claimedConstraints.size(), 3 );
    BOOST_CHECK( matches[0].claimedConstraints.count( TRACK_WIDTH_CONSTRAINT ) > 0 );
    BOOST_CHECK( matches[0].claimedConstraints.count( DIFF_PAIR_GAP_CONSTRAINT ) > 0 );
    BOOST_CHECK( matches[0].claimedConstraints.count( MAX_UNCOUPLED_CONSTRAINT ) > 0 );
}

BOOST_AUTO_TEST_CASE( PanelMatcherTrackWidthOnly )
{
    // Test: Rule with only track_width → ROUTING_WIDTH (not ROUTING_DIFF_PAIR)
    DRC_RULE rule( "TrackWidthOnlyRule" );

    DRC_CONSTRAINT trackWidth( TRACK_WIDTH_CONSTRAINT );
    trackWidth.Value().SetMin( 200000 );
    trackWidth.Value().SetOpt( 250000 );
    trackWidth.Value().SetMax( 300000 );
    rule.AddConstraint( trackWidth );

    DRC_PANEL_MATCHER matcher;
    std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );

    BOOST_REQUIRE_EQUAL( matches.size(), 1 );
    BOOST_CHECK_EQUAL( matches[0].panelType, ROUTING_WIDTH );
    BOOST_CHECK( matches[0].claimedConstraints.count( TRACK_WIDTH_CONSTRAINT ) > 0 );
}

BOOST_AUTO_TEST_CASE( PanelMatcherTextHeightAndThickness )
{
    // Test: Rule with text_height + text_thickness → MINIMUM_TEXT_HEIGHT_AND_THICKNESS
    DRC_RULE rule( "TextRule" );

    DRC_CONSTRAINT textHeight( TEXT_HEIGHT_CONSTRAINT );
    textHeight.Value().SetMin( 1000000 );
    rule.AddConstraint( textHeight );

    DRC_CONSTRAINT textThickness( TEXT_THICKNESS_CONSTRAINT );
    textThickness.Value().SetMin( 150000 );
    rule.AddConstraint( textThickness );

    DRC_PANEL_MATCHER matcher;
    std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );

    BOOST_REQUIRE_EQUAL( matches.size(), 1 );
    BOOST_CHECK_EQUAL( matches[0].panelType, MINIMUM_TEXT_HEIGHT_AND_THICKNESS );
    BOOST_CHECK_EQUAL( matches[0].claimedConstraints.size(), 2 );
}

BOOST_AUTO_TEST_CASE( PanelMatcherLengthConstraint )
{
    // Test: Rule with length constraint matches ABSOLUTE_LENGTH panel
    DRC_RULE rule( "LengthRule" );

    DRC_CONSTRAINT length( LENGTH_CONSTRAINT );
    length.Value().SetMin( 10000000 );
    length.Value().SetOpt( 30000000 );
    length.Value().SetMax( 50000000 );
    rule.AddConstraint( length );

    DRC_PANEL_MATCHER matcher;
    std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );

    BOOST_REQUIRE_EQUAL( matches.size(), 1 );
    BOOST_CHECK_EQUAL( matches[0].panelType, ABSOLUTE_LENGTH );
    BOOST_CHECK( matches[0].claimedConstraints.count( LENGTH_CONSTRAINT ) > 0 );
}

BOOST_AUTO_TEST_CASE( PanelMatcherLengthAndSkewConstraint )
{
    // Rule with length + skew should match MATCHED_LENGTH_DIFF_PAIR, not ABSOLUTE_LENGTH
    DRC_RULE rule( "MatchedLengthRule" );

    DRC_CONSTRAINT length( LENGTH_CONSTRAINT );
    length.Value().SetMin( 10000000 );
    length.Value().SetOpt( 30000000 );
    length.Value().SetMax( 50000000 );
    rule.AddConstraint( length );

    DRC_CONSTRAINT skew( SKEW_CONSTRAINT );
    skew.Value().SetMax( 1000000 );
    rule.AddConstraint( skew );

    DRC_PANEL_MATCHER matcher;
    std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );

    BOOST_REQUIRE_EQUAL( matches.size(), 1 );
    BOOST_CHECK_EQUAL( matches[0].panelType, MATCHED_LENGTH_DIFF_PAIR );
    BOOST_CHECK( matches[0].claimedConstraints.count( LENGTH_CONSTRAINT ) > 0 );
    BOOST_CHECK( matches[0].claimedConstraints.count( SKEW_CONSTRAINT ) > 0 );
}

BOOST_AUTO_TEST_CASE( PanelMatcherEmptyRule )
{
    // Test: Rule with no constraints → no matches
    DRC_RULE rule( "EmptyRule" );

    DRC_PANEL_MATCHER matcher;
    std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );

    BOOST_CHECK_EQUAL( matches.size(), 0 );
}

BOOST_AUTO_TEST_CASE( PanelMatcherCanPanelLoad )
{
    DRC_PANEL_MATCHER matcher;

    // VIA_STYLE can load via_diameter + hole_size
    std::set<DRC_CONSTRAINT_T> viaStyleConstraints = { VIA_DIAMETER_CONSTRAINT, HOLE_SIZE_CONSTRAINT };
    BOOST_CHECK( matcher.CanPanelLoad( VIA_STYLE, viaStyleConstraints ) );

    // VIA_STYLE cannot load clearance
    std::set<DRC_CONSTRAINT_T> clearanceConstraint = { CLEARANCE_CONSTRAINT };
    BOOST_CHECK( !matcher.CanPanelLoad( VIA_STYLE, clearanceConstraint ) );

    // CUSTOM_RULE can load anything
    BOOST_CHECK( matcher.CanPanelLoad( CUSTOM_RULE, viaStyleConstraints ) );
    BOOST_CHECK( matcher.CanPanelLoad( CUSTOM_RULE, clearanceConstraint ) );
}

BOOST_AUTO_TEST_CASE( PanelMatcherGetPanelForConstraint )
{
    DRC_PANEL_MATCHER matcher;

    // Single constraint lookups
    BOOST_CHECK_EQUAL( matcher.GetPanelForConstraint( CLEARANCE_CONSTRAINT ), MINIMUM_CLEARANCE );
    BOOST_CHECK_EQUAL( matcher.GetPanelForConstraint( EDGE_CLEARANCE_CONSTRAINT ), COPPER_TO_EDGE_CLEARANCE );
    BOOST_CHECK_EQUAL( matcher.GetPanelForConstraint( HOLE_CLEARANCE_CONSTRAINT ), COPPER_TO_HOLE_CLEARANCE );
    BOOST_CHECK_EQUAL( matcher.GetPanelForConstraint( HOLE_TO_HOLE_CONSTRAINT ), HOLE_TO_HOLE_DISTANCE );
    BOOST_CHECK_EQUAL( matcher.GetPanelForConstraint( SILK_CLEARANCE_CONSTRAINT ), SILK_TO_SILK_CLEARANCE );
    BOOST_CHECK_EQUAL( matcher.GetPanelForConstraint( TRACK_WIDTH_CONSTRAINT ), ROUTING_WIDTH );
}


// ============================================================================
// Rule Loader Tests (Phase 5.1)
// ============================================================================

BOOST_AUTO_TEST_CASE( RuleLoaderViaStyleFromText )
{
    // Test: Load a via style rule from text and verify values
    wxString ruleText =
        "(version 1)\n"
        "(rule \"Via Test\"\n"
        "    (constraint via_diameter (min 0.5mm) (opt 0.6mm) (max 0.8mm))\n"
        "    (constraint hole_size (min 0.2mm) (opt 0.3mm) (max 0.4mm)))";

    DRC_RULE_LOADER loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );

    BOOST_REQUIRE_EQUAL( entries.size(), 1 );
    BOOST_CHECK_EQUAL( entries[0].panelType, VIA_STYLE );
    BOOST_CHECK_EQUAL( entries[0].ruleName, "Via Test" );

    auto viaData = std::dynamic_pointer_cast<DRC_RE_VIA_STYLE_CONSTRAINT_DATA>( entries[0].constraintData );
    BOOST_REQUIRE( viaData );

    BOOST_CHECK_CLOSE( viaData->GetMinViaDiameter(), 0.5, 0.0001 );
    BOOST_CHECK_CLOSE( viaData->GetMaxViaDiameter(), 0.8, 0.0001 );
    BOOST_CHECK_CLOSE( viaData->GetMinViaHoleSize(), 0.2, 0.0001 );
    BOOST_CHECK_CLOSE( viaData->GetMaxViaHoleSize(), 0.4, 0.0001 );
}

BOOST_AUTO_TEST_CASE( RuleLoaderRoutingWidthFromText )
{
    // Test: Load a routing width rule from text
    wxString ruleText =
        "(version 1)\n"
        "(rule \"Track Test\"\n"
        "    (constraint track_width (min 0.2mm) (opt 0.25mm) (max 0.3mm)))";

    DRC_RULE_LOADER loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );

    BOOST_REQUIRE_EQUAL( entries.size(), 1 );
    BOOST_CHECK_EQUAL( entries[0].panelType, ROUTING_WIDTH );
    BOOST_CHECK_EQUAL( entries[0].ruleName, "Track Test" );

    auto trackData = std::dynamic_pointer_cast<DRC_RE_ROUTING_WIDTH_CONSTRAINT_DATA>( entries[0].constraintData );
    BOOST_REQUIRE( trackData );

    BOOST_CHECK_CLOSE( trackData->GetOptWidth(), 0.25, 0.0001 );
    BOOST_CHECK_CLOSE( trackData->GetWidthTolerance(), 0.05, 0.0001 );
}

BOOST_AUTO_TEST_CASE( RuleLoaderDiffPairFromText )
{
    // Test: Load a diff pair rule from text
    wxString ruleText =
        "(version 1)\n"
        "(rule \"Diff Pair Test\"\n"
        "    (constraint track_width (min 0.2mm) (opt 0.25mm) (max 0.3mm))\n"
        "    (constraint diff_pair_gap (min 0.1mm) (opt 0.15mm) (max 0.2mm))\n"
        "    (constraint diff_pair_uncoupled (max 5mm)))";

    DRC_RULE_LOADER loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );

    BOOST_REQUIRE_EQUAL( entries.size(), 1 );
    BOOST_CHECK_EQUAL( entries[0].panelType, ROUTING_DIFF_PAIR );

    auto dpData = std::dynamic_pointer_cast<DRC_RE_ROUTING_DIFF_PAIR_CONSTRAINT_DATA>( entries[0].constraintData );
    BOOST_REQUIRE( dpData );

    BOOST_CHECK_CLOSE( dpData->GetOptWidth(), 0.25, 0.0001 );
    BOOST_CHECK_CLOSE( dpData->GetWidthTolerance(), 0.05, 0.0001 );
    BOOST_CHECK_CLOSE( dpData->GetOptGap(), 0.15, 0.0001 );
    BOOST_CHECK_CLOSE( dpData->GetGapTolerance(), 0.05, 0.0001 );
    BOOST_CHECK_CLOSE( dpData->GetMaxUncoupledLength(), 5.0, 0.0001 );
}

BOOST_AUTO_TEST_CASE( RuleLoaderSplitRuleFromText )
{
    // Test: Load a rule that splits into multiple panels
    wxString ruleText =
        "(version 1)\n"
        "(rule \"Split Test\"\n"
        "    (constraint via_diameter (min 0.5mm))\n"
        "    (constraint hole_size (min 0.2mm))\n"
        "    (constraint clearance (min 0.15mm)))";

    DRC_RULE_LOADER loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );

    BOOST_REQUIRE_EQUAL( entries.size(), 2 );

    // First entry should be VIA_STYLE
    BOOST_CHECK_EQUAL( entries[0].panelType, VIA_STYLE );
    BOOST_CHECK_EQUAL( entries[0].ruleName, "Split Test" );

    // Second entry should be MINIMUM_CLEARANCE
    BOOST_CHECK_EQUAL( entries[1].panelType, MINIMUM_CLEARANCE );
    BOOST_CHECK_EQUAL( entries[1].ruleName, "Split Test" );

    auto numericData = std::dynamic_pointer_cast<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>( entries[1].constraintData );
    BOOST_REQUIRE( numericData );
    BOOST_CHECK_CLOSE( numericData->GetNumericInputValue(), 0.15, 0.0001 );
}

BOOST_AUTO_TEST_CASE( RuleLoaderVmeWrenClearanceUnderFpga )
{
    // Test: Load the clearance_under_fpga rule from vme-wren demo which should split
    // This rule has: clearance, hole_size, via_diameter
    // Should split into: VIA_STYLE (hole_size + via_diameter) + MINIMUM_CLEARANCE (clearance)
    wxString ruleText =
        "(version 1)\n"
        "(rule \"clearance_under_fpga\"\n"
        "    (constraint clearance (min 0.1mm))\n"
        "    (constraint hole_size (min 0.2mm))\n"
        "    (constraint via_diameter (min 0.4mm))\n"
        "    (condition \"A.intersectsArea('underFPGA') || A.intersectsArea('underDDR')\"))";

    DRC_RULE_LOADER loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );

    // Should produce 2 entries from the split
    BOOST_REQUIRE_EQUAL( entries.size(), 2 );

    // First entry should be VIA_STYLE (claims via_diameter + hole_size at priority 90)
    BOOST_CHECK_EQUAL( entries[0].panelType, VIA_STYLE );
    BOOST_CHECK_EQUAL( entries[0].ruleName, "clearance_under_fpga" );
    BOOST_CHECK_EQUAL( entries[0].condition, "A.intersectsArea('underFPGA') || A.intersectsArea('underDDR')" );

    // Second entry should be MINIMUM_CLEARANCE (claims clearance at priority 30)
    BOOST_CHECK_EQUAL( entries[1].panelType, MINIMUM_CLEARANCE );
    BOOST_CHECK_EQUAL( entries[1].ruleName, "clearance_under_fpga" );
    BOOST_CHECK_EQUAL( entries[1].condition, "A.intersectsArea('underFPGA') || A.intersectsArea('underDDR')" );

    // Verify VIA_STYLE has correct values
    auto viaData = std::dynamic_pointer_cast<DRC_RE_VIA_STYLE_CONSTRAINT_DATA>( entries[0].constraintData );
    BOOST_REQUIRE( viaData );
    BOOST_CHECK_CLOSE( viaData->GetMinViaDiameter(), 0.4, 0.0001 );
    BOOST_CHECK_CLOSE( viaData->GetMinViaHoleSize(), 0.2, 0.0001 );

    // Verify MINIMUM_CLEARANCE has correct value
    auto clearanceData = std::dynamic_pointer_cast<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>( entries[1].constraintData );
    BOOST_REQUIRE( clearanceData );
    BOOST_CHECK_CLOSE( clearanceData->GetNumericInputValue(), 0.1, 0.0001 );
}

BOOST_AUTO_TEST_CASE( RuleLoaderTextHeightThicknessFromText )
{
    // Test: Load text height and thickness rule
    wxString ruleText =
        "(version 1)\n"
        "(rule \"Text Test\"\n"
        "    (constraint text_height (min 1mm))\n"
        "    (constraint text_thickness (min 0.15mm)))";

    DRC_RULE_LOADER loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );

    BOOST_REQUIRE_EQUAL( entries.size(), 1 );
    BOOST_CHECK_EQUAL( entries[0].panelType, MINIMUM_TEXT_HEIGHT_AND_THICKNESS );

    auto textData = std::dynamic_pointer_cast<DRC_RE_MINIMUM_TEXT_HEIGHT_THICKNESS_CONSTRAINT_DATA>( entries[0].constraintData );
    BOOST_REQUIRE( textData );

    BOOST_CHECK_CLOSE( textData->GetMinTextHeight(), 1.0, 0.0001 );
    BOOST_CHECK_CLOSE( textData->GetMinTextThickness(), 0.15, 0.0001 );
}

BOOST_AUTO_TEST_CASE( RuleLoaderAbsoluteLengthFromText )
{
    // Test: Load absolute length rule with min/opt/max
    wxString ruleText =
        "(version 1)\n"
        "(rule \"Length Test\"\n"
        "    (constraint length (min 10mm) (opt 30mm) (max 50mm)))";

    DRC_RULE_LOADER loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );

    BOOST_REQUIRE_EQUAL( entries.size(), 1 );
    BOOST_CHECK_EQUAL( entries[0].panelType, ABSOLUTE_LENGTH );
    BOOST_CHECK_EQUAL( entries[0].ruleName, "Length Test" );

    auto lengthData = dynamic_pointer_cast<DRC_RE_ABSOLUTE_LENGTH_TWO_CONSTRAINT_DATA>( entries[0].constraintData );
    BOOST_REQUIRE( lengthData );
    BOOST_CHECK_CLOSE( lengthData->GetMinimumLength(), 10.0, 0.0001 );
    BOOST_CHECK_CLOSE( lengthData->GetOptimumLength(), 30.0, 0.0001 );
    BOOST_CHECK_CLOSE( lengthData->GetMaximumLength(), 50.0, 0.0001 );
}

BOOST_AUTO_TEST_CASE( RuleLoaderMatchedLengthWithSkew )
{
    // Rule with length + skew should load as MATCHED_LENGTH_DIFF_PAIR and preserve skew
    wxString ruleText =
        "(version 1)\n"
        "(rule \"DP Length Match\"\n"
        "    (constraint length (min 10mm) (opt 30mm) (max 50mm))\n"
        "    (constraint skew (max 1mm)))";

    DRC_RULE_LOADER loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );

    BOOST_REQUIRE_EQUAL( entries.size(), 1 );
    BOOST_CHECK_EQUAL( entries[0].panelType, MATCHED_LENGTH_DIFF_PAIR );
    BOOST_CHECK_EQUAL( entries[0].ruleName, "DP Length Match" );

    auto matchedData =
            std::dynamic_pointer_cast<DRC_RE_MATCHED_LENGTH_DIFF_PAIR_CONSTRAINT_DATA>( entries[0].constraintData );
    BOOST_REQUIRE( matchedData );

    BOOST_CHECK_CLOSE( matchedData->GetMinimumLength(), 10.0, 0.0001 );
    BOOST_CHECK_CLOSE( matchedData->GetOptimumLength(), 30.0, 0.0001 );
    BOOST_CHECK_CLOSE( matchedData->GetMaximumLength(), 50.0, 0.0001 );
    BOOST_CHECK_CLOSE( matchedData->GetMaxSkew(), 1.0, 0.0001 );
}

BOOST_AUTO_TEST_CASE( RuleLoaderMatchedLengthRoundTrip )
{
    // Verify length+skew rule round-trips without losing skew
    wxString ruleText =
        "(version 1)\n"
        "(rule \"DP Match RT\"\n"
        "    (constraint length (min 10mm) (opt 30mm) (max 50mm))\n"
        "    (constraint skew (max 2.5mm)))";

    DRC_RULE_LOADER loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );

    BOOST_REQUIRE_EQUAL( entries.size(), 1 );
    entries[0].wasEdited = true;

    DRC_RULE_SAVER saver;
    wxString savedText = saver.GenerateRulesText( entries, nullptr );

    BOOST_CHECK( savedText.Contains( "length" ) );
    BOOST_CHECK( savedText.Contains( "skew" ) );
    BOOST_CHECK( savedText.Contains( "2.5mm" ) );

    std::vector<DRC_RE_LOADED_PANEL_ENTRY> reloaded = loader.LoadFromString( savedText );

    BOOST_REQUIRE_EQUAL( reloaded.size(), 1 );
    BOOST_CHECK_EQUAL( reloaded[0].panelType, MATCHED_LENGTH_DIFF_PAIR );

    auto reloadedData =
            std::dynamic_pointer_cast<DRC_RE_MATCHED_LENGTH_DIFF_PAIR_CONSTRAINT_DATA>( reloaded[0].constraintData );
    BOOST_REQUIRE( reloadedData );

    BOOST_CHECK_CLOSE( reloadedData->GetMaxSkew(), 2.5, 0.0001 );
    BOOST_CHECK_CLOSE( reloadedData->GetMinimumLength(), 10.0, 0.0001 );
    BOOST_CHECK_CLOSE( reloadedData->GetOptimumLength(), 30.0, 0.0001 );
    BOOST_CHECK_CLOSE( reloadedData->GetMaximumLength(), 50.0, 0.0001 );
}

BOOST_AUTO_TEST_CASE( RuleLoaderMatchedLengthWithinDiffPairs )
{
    // Rule with skew + within_diff_pairs should load and preserve the option
    wxString ruleText = "(version 1)\n"
                        "(rule \"DP Skew Within\"\n"
                        "    (constraint length (min 10mm) (opt 30mm) (max 50mm))\n"
                        "    (constraint skew (max 1mm) (within_diff_pairs)))";

    DRC_RULE_LOADER                        loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );

    BOOST_REQUIRE_EQUAL( entries.size(), 1 );
    BOOST_CHECK_EQUAL( entries[0].panelType, MATCHED_LENGTH_DIFF_PAIR );

    auto matchedData =
            std::dynamic_pointer_cast<DRC_RE_MATCHED_LENGTH_DIFF_PAIR_CONSTRAINT_DATA>( entries[0].constraintData );
    BOOST_REQUIRE( matchedData );

    BOOST_CHECK_CLOSE( matchedData->GetMaxSkew(), 1.0, 0.0001 );
    BOOST_CHECK( matchedData->GetWithinDiffPairs() );

    // Round-trip: save and reload
    entries[0].wasEdited = true;

    DRC_RULE_SAVER saver;
    wxString       savedText = saver.GenerateRulesText( entries, nullptr );

    BOOST_CHECK( savedText.Contains( "within_diff_pairs" ) );

    std::vector<DRC_RE_LOADED_PANEL_ENTRY> reloaded = loader.LoadFromString( savedText );

    BOOST_REQUIRE_EQUAL( reloaded.size(), 1 );

    auto reloadedData =
            std::dynamic_pointer_cast<DRC_RE_MATCHED_LENGTH_DIFF_PAIR_CONSTRAINT_DATA>( reloaded[0].constraintData );
    BOOST_REQUIRE( reloadedData );

    BOOST_CHECK_CLOSE( reloadedData->GetMaxSkew(), 1.0, 0.0001 );
    BOOST_CHECK( reloadedData->GetWithinDiffPairs() );
}

BOOST_AUTO_TEST_CASE( RuleLoaderMatchedLengthWithoutWithinDiffPairs )
{
    // Rule without within_diff_pairs should default to false
    wxString ruleText = "(version 1)\n"
                        "(rule \"DP Skew Bus\"\n"
                        "    (constraint length (min 10mm) (opt 30mm) (max 50mm))\n"
                        "    (constraint skew (max 2mm)))";

    DRC_RULE_LOADER                        loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );

    BOOST_REQUIRE_EQUAL( entries.size(), 1 );

    auto matchedData =
            std::dynamic_pointer_cast<DRC_RE_MATCHED_LENGTH_DIFF_PAIR_CONSTRAINT_DATA>( entries[0].constraintData );
    BOOST_REQUIRE( matchedData );

    BOOST_CHECK( !matchedData->GetWithinDiffPairs() );

    // Round-trip should NOT contain within_diff_pairs
    entries[0].wasEdited = true;

    DRC_RULE_SAVER saver;
    wxString       savedText = saver.GenerateRulesText( entries, nullptr );

    BOOST_CHECK( !savedText.Contains( "within_diff_pairs" ) );
}

BOOST_AUTO_TEST_CASE( RuleLoaderWithCondition )
{
    // Test: Load rule with condition
    wxString ruleText =
        "(version 1)\n"
        "(rule \"Conditional Test\"\n"
        "    (condition \"A.NetClass == 'Power'\")\n"
        "    (constraint clearance (min 0.3mm)))";

    DRC_RULE_LOADER loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );

    BOOST_REQUIRE_EQUAL( entries.size(), 1 );
    BOOST_CHECK_EQUAL( entries[0].ruleName, "Conditional Test" );
    BOOST_CHECK_EQUAL( entries[0].condition, "A.NetClass == 'Power'" );
    BOOST_CHECK_EQUAL( entries[0].constraintData->GetRuleCondition(), "A.NetClass == 'Power'" );
}

BOOST_AUTO_TEST_CASE( RuleLoaderMultipleRules )
{
    // Test: Load multiple rules and verify order preservation
    wxString ruleText =
        "(version 1)\n"
        "(rule \"Rule A\"\n"
        "    (constraint clearance (min 0.2mm)))\n"
        "(rule \"Rule B\"\n"
        "    (constraint track_width (min 0.15mm)))\n"
        "(rule \"Rule C\"\n"
        "    (constraint via_diameter (min 0.5mm))\n"
        "    (constraint hole_size (min 0.3mm)))";

    DRC_RULE_LOADER loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );

    BOOST_REQUIRE_EQUAL( entries.size(), 3 );

    BOOST_CHECK_EQUAL( entries[0].ruleName, "Rule A" );
    BOOST_CHECK_EQUAL( entries[0].panelType, MINIMUM_CLEARANCE );

    BOOST_CHECK_EQUAL( entries[1].ruleName, "Rule B" );
    BOOST_CHECK_EQUAL( entries[1].panelType, ROUTING_WIDTH );

    BOOST_CHECK_EQUAL( entries[2].ruleName, "Rule C" );
    BOOST_CHECK_EQUAL( entries[2].panelType, VIA_STYLE );
}

BOOST_AUTO_TEST_CASE( RuleLoaderEmptyRule )
{
    // Test: Rule with no constraints creates custom rule fallback
    wxString ruleText =
        "(version 1)\n"
        "(rule \"Empty Rule\")";

    DRC_RULE_LOADER loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );

    BOOST_REQUIRE_EQUAL( entries.size(), 1 );
    BOOST_CHECK_EQUAL( entries[0].panelType, CUSTOM_RULE );
    BOOST_CHECK_EQUAL( entries[0].ruleName, "Empty Rule" );
}

BOOST_AUTO_TEST_CASE( RuleLoaderInvalidText )
{
    // Test: Invalid rule text returns empty vector
    wxString ruleText = "not valid rule text at all";

    DRC_RULE_LOADER loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );

    BOOST_CHECK_EQUAL( entries.size(), 0 );
}

BOOST_AUTO_TEST_CASE( RuleLoaderNumericConstraints )
{
    // Test various numeric constraints map to correct panels
    wxString ruleText =
        "(version 1)\n"
        "(rule \"Edge Clearance\"\n"
        "    (constraint edge_clearance (min 0.5mm)))\n"
        "(rule \"Hole Clearance\"\n"
        "    (constraint hole_clearance (min 0.3mm)))\n"
        "(rule \"Courtyard\"\n"
        "    (constraint courtyard_clearance (min 0.25mm)))";

    DRC_RULE_LOADER loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );

    BOOST_REQUIRE_EQUAL( entries.size(), 3 );

    BOOST_CHECK_EQUAL( entries[0].panelType, COPPER_TO_EDGE_CLEARANCE );
    BOOST_CHECK_EQUAL( entries[1].panelType, COPPER_TO_HOLE_CLEARANCE );
    BOOST_CHECK_EQUAL( entries[2].panelType, COURTYARD_CLEARANCE );
}


// ============================================================================
// Rule Saver Tests (Phase 3)
// ============================================================================

BOOST_AUTO_TEST_CASE( RuleSaverBasicGeneration )
{
    // Test: Basic rule text generation from panel entry
    DRC_RE_LOADED_PANEL_ENTRY entry;
    entry.panelType = MINIMUM_CLEARANCE;
    entry.ruleName = "TestClearance";
    entry.wasEdited = true;

    auto numericData = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>();
    numericData->SetRuleName( "TestClearance" );
    numericData->SetConstraintCode( "clearance" );
    numericData->SetNumericInputValue( 0.2 );
    entry.constraintData = numericData;

    DRC_RULE_SAVER saver;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
    wxString result = saver.GenerateRulesText( entries, nullptr );

    BOOST_CHECK( result.Contains( "(version 1)" ) );
    BOOST_CHECK( result.Contains( "TestClearance" ) );
    BOOST_CHECK( result.Contains( "clearance" ) );
}

BOOST_AUTO_TEST_CASE( RuleSaverRoundTripPreservation )
{
    // Test: Original text preserved when not edited
    DRC_RE_LOADED_PANEL_ENTRY entry;
    entry.panelType = MINIMUM_CLEARANCE;
    entry.ruleName = "Preserved Rule";
    entry.originalRuleText = "(rule \"Preserved Rule\"\n\t(constraint clearance (min 0.5mm)))";
    entry.wasEdited = false;

    auto numericData = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>();
    numericData->SetRuleName( "Preserved Rule" );
    numericData->SetConstraintCode( "clearance" );
    numericData->SetNumericInputValue( 0.5 );
    entry.constraintData = numericData;

    DRC_RULE_SAVER saver;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
    wxString result = saver.GenerateRulesText( entries, nullptr );

    BOOST_CHECK( result.Contains( entry.originalRuleText ) );
}

BOOST_AUTO_TEST_CASE( RuleSaverEditedRuleRegenerated )
{
    // Test: Edited rules regenerate from panel data
    DRC_RE_LOADED_PANEL_ENTRY entry;
    entry.panelType = MINIMUM_CLEARANCE;
    entry.ruleName = "EditedRule";
    entry.originalRuleText = "(rule \"EditedRule\"\n\t(constraint clearance (min 0.5mm)))";
    entry.wasEdited = true;

    auto numericData = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>();
    numericData->SetRuleName( "EditedRule" );
    numericData->SetConstraintCode( "clearance" );
    numericData->SetNumericInputValue( 0.3 );
    entry.constraintData = numericData;

    DRC_RULE_SAVER saver;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
    wxString result = saver.GenerateRulesText( entries, nullptr );

    // Original text should NOT be preserved since wasEdited is true
    BOOST_CHECK( !result.Contains( "0.5mm" ) );
    BOOST_CHECK( result.Contains( "EditedRule" ) );
}

BOOST_AUTO_TEST_CASE( RuleSaverViaStyleRule )
{
    // Test: Via style rule generation
    DRC_RE_LOADED_PANEL_ENTRY entry;
    entry.panelType = VIA_STYLE;
    entry.ruleName = "ViaTest";

    auto viaData = std::make_shared<DRC_RE_VIA_STYLE_CONSTRAINT_DATA>();
    viaData->SetRuleName( "ViaTest" );
    viaData->SetMinViaDiameter( 0.5 );
    viaData->SetMaxViaDiameter( 0.8 );
    viaData->SetMinViaHoleSize( 0.2 );
    viaData->SetMaxViaHoleSize( 0.4 );
    entry.constraintData = viaData;
    entry.wasEdited = true;

    DRC_RULE_SAVER saver;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
    wxString result = saver.GenerateRulesText( entries, nullptr );

    BOOST_CHECK( result.Contains( "ViaTest" ) );
    BOOST_CHECK( result.Contains( "via_diameter" ) );
    BOOST_CHECK( result.Contains( "hole_size" ) );
}

BOOST_AUTO_TEST_CASE( RuleSaverMultipleEntries )
{
    // Test: Multiple entries saved in order
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries;

    DRC_RE_LOADED_PANEL_ENTRY entry1;
    entry1.panelType = MINIMUM_CLEARANCE;
    entry1.ruleName = "RuleA";
    auto data1 = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>();
    data1->SetRuleName( "RuleA" );
    data1->SetConstraintCode( "clearance" );
    data1->SetNumericInputValue( 0.2 );
    entry1.constraintData = data1;
    entry1.wasEdited = true;
    entries.push_back( entry1 );

    DRC_RE_LOADED_PANEL_ENTRY entry2;
    entry2.panelType = ROUTING_WIDTH;
    entry2.ruleName = "RuleB";
    auto data2 = std::make_shared<DRC_RE_ROUTING_WIDTH_CONSTRAINT_DATA>();
    data2->SetRuleName( "RuleB" );
    data2->SetOptWidth( 0.2 );
    data2->SetWidthTolerance( 0.1 );
    entry2.constraintData = data2;
    entry2.wasEdited = true;
    entries.push_back( entry2 );

    DRC_RULE_SAVER saver;
    wxString result = saver.GenerateRulesText( entries, nullptr );

    // Both rules should appear
    BOOST_CHECK( result.Contains( "RuleA" ) );
    BOOST_CHECK( result.Contains( "RuleB" ) );

    // Check order: RuleA should appear before RuleB
    size_t posA = result.Find( "RuleA" );
    size_t posB = result.Find( "RuleB" );
    BOOST_CHECK( posA < posB );
}

BOOST_AUTO_TEST_CASE( RuleSaverWithCondition )
{
    // Test: Rule with condition
    DRC_RE_LOADED_PANEL_ENTRY entry;
    entry.panelType = MINIMUM_CLEARANCE;
    entry.ruleName = "ConditionalRule";
    entry.condition = "A.NetClass == 'Power'";

    auto numericData = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>();
    numericData->SetRuleName( "ConditionalRule" );
    numericData->SetConstraintCode( "clearance" );
    numericData->SetRuleCondition( "A.NetClass == 'Power'" );
    numericData->SetNumericInputValue( 0.3 );
    entry.constraintData = numericData;
    entry.wasEdited = true;

    DRC_RULE_SAVER saver;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
    wxString result = saver.GenerateRulesText( entries, nullptr );

    BOOST_CHECK( result.Contains( "ConditionalRule" ) );
    BOOST_CHECK( result.Contains( "condition" ) );
    BOOST_CHECK( result.Contains( "Power" ) );
}

BOOST_AUTO_TEST_CASE( RuleSaverEmptyEntries )
{
    // Test: Empty entries vector
    DRC_RULE_SAVER saver;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries;
    wxString result = saver.GenerateRulesText( entries, nullptr );

    BOOST_CHECK( result.Contains( "(version 1)" ) );
    // Should have version header but nothing else
    BOOST_CHECK_EQUAL( result.Trim(), "(version 1)" );
}

BOOST_AUTO_TEST_CASE( RuleSaverNullConstraintData )
{
    // Test: Entry with null constraint data is skipped
    DRC_RE_LOADED_PANEL_ENTRY entry;
    entry.panelType = MINIMUM_CLEARANCE;
    entry.ruleName = "Null Data Rule";
    entry.constraintData = nullptr;
    entry.wasEdited = true;

    DRC_RULE_SAVER saver;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
    wxString result = saver.GenerateRulesText( entries, nullptr );

    // Should have version header but rule text is skipped
    BOOST_CHECK( result.Contains( "(version 1)" ) );
    BOOST_CHECK( !result.Contains( "Null Data Rule" ) );
}

BOOST_AUTO_TEST_CASE( RuleSaverLoadSaveRoundTrip )
{
    // Test: Full round-trip: Load → mark edited → Save → Load produces equivalent data
    wxString originalText =
        "(version 1)\n"
        "(rule \"RoundTripTest\"\n"
        "    (constraint clearance (min 0.25mm)))";

    DRC_RULE_LOADER loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( originalText );

    BOOST_REQUIRE_EQUAL( entries.size(), 1 );
    BOOST_CHECK_EQUAL( entries[0].ruleName, "RoundTripTest" );

    // Mark as edited so it regenerates from data (loader doesn't preserve exact original text)
    entries[0].wasEdited = true;

    // Save
    DRC_RULE_SAVER saver;
    wxString savedText = saver.GenerateRulesText( entries, nullptr );

    // Verify saved text is valid by reloading
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> reloadedEntries = loader.LoadFromString( savedText );

    BOOST_REQUIRE_EQUAL( reloadedEntries.size(), 1 );
    BOOST_CHECK_EQUAL( reloadedEntries[0].ruleName, "RoundTripTest" );
    BOOST_CHECK_EQUAL( reloadedEntries[0].panelType, entries[0].panelType );
}

BOOST_AUTO_TEST_CASE( RuleSaverPreservesQuotedNameWithSpaces )
{
    // Issue 23852: editing a rule named like "Annular width: LEDs" must not rewrite
    // the name to Annular_width__LEDs.  Names with whitespace or other characters
    // that require quoting must be emitted as a quoted string and round-trip intact.
    const wxString originalName = wxS( "Annular width: LEDs" );

    DRC_RE_LOADED_PANEL_ENTRY entry;
    entry.panelType = MINIMUM_CLEARANCE;
    entry.ruleName = originalName;
    entry.wasEdited = true;

    auto numericData = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>();
    numericData->SetRuleName( originalName );
    numericData->SetConstraintCode( "annular_width" );
    numericData->SetNumericInputValue( 0.085 );
    entry.constraintData = numericData;

    DRC_RULE_SAVER saver;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
    wxString result = saver.GenerateRulesText( entries, nullptr );

    // The saver must emit the name in quotes, not mangle it with underscores.
    BOOST_CHECK( result.Contains( wxS( "\"Annular width: LEDs\"" ) ) );
    BOOST_CHECK( !result.Contains( wxS( "Annular_width__LEDs" ) ) );

    // Reload the saved text and verify the rule name survives unchanged.
    DRC_RULE_LOADER loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> reloaded = loader.LoadFromString( result );

    BOOST_REQUIRE_EQUAL( reloaded.size(), 1 );
    BOOST_CHECK_EQUAL( reloaded[0].ruleName, originalName );
}


BOOST_AUTO_TEST_CASE( RuleSaverPreservesBareSymbolNameUnquoted )
{
    // A bare symbol rule name should still be emitted without quotes.
    DRC_RE_LOADED_PANEL_ENTRY entry;
    entry.panelType = MINIMUM_CLEARANCE;
    entry.ruleName = wxS( "My_Bare_Rule-1.0" );
    entry.wasEdited = true;

    auto numericData = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>();
    numericData->SetRuleName( entry.ruleName );
    numericData->SetConstraintCode( "clearance" );
    numericData->SetNumericInputValue( 0.2 );
    entry.constraintData = numericData;

    DRC_RULE_SAVER saver;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
    wxString result = saver.GenerateRulesText( entries, nullptr );

    BOOST_CHECK( result.Contains( wxS( "(rule My_Bare_Rule-1.0\n" ) ) );
    BOOST_CHECK( !result.Contains( wxS( "\"My_Bare_Rule-1.0\"" ) ) );
}


BOOST_AUTO_TEST_CASE( RuleSaverQuotesNameStartingWithDigit )
{
    // Names starting with a digit must be quoted so S-expression parsers treat them
    // as a symbol rather than a numeric literal.
    DRC_RE_LOADED_PANEL_ENTRY entry;
    entry.panelType = MINIMUM_CLEARANCE;
    entry.ruleName = wxS( "3.3V_Power" );
    entry.wasEdited = true;

    auto numericData = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>();
    numericData->SetRuleName( entry.ruleName );
    numericData->SetConstraintCode( "clearance" );
    numericData->SetNumericInputValue( 0.2 );
    entry.constraintData = numericData;

    DRC_RULE_SAVER saver;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
    wxString result = saver.GenerateRulesText( entries, nullptr );

    BOOST_CHECK( result.Contains( wxS( "\"3.3V_Power\"" ) ) );

    DRC_RULE_LOADER loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> reloaded = loader.LoadFromString( result );
    BOOST_REQUIRE_EQUAL( reloaded.size(), 1 );
    BOOST_CHECK_EQUAL( reloaded[0].ruleName, "3.3V_Power" );
}


BOOST_AUTO_TEST_CASE( RuleSaverDiffPairRule )
{
    // Test: Diff pair rule generation
    DRC_RE_LOADED_PANEL_ENTRY entry;
    entry.panelType = ROUTING_DIFF_PAIR;
    entry.ruleName = "DiffPairTest";

    auto dpData = std::make_shared<DRC_RE_ROUTING_DIFF_PAIR_CONSTRAINT_DATA>();
    dpData->SetRuleName( "DiffPairTest" );
    dpData->SetOptWidth( 0.25 );
    dpData->SetWidthTolerance( 0.05 );
    dpData->SetOptGap( 0.15 );
    dpData->SetGapTolerance( 0.05 );
    dpData->SetMaxUncoupledLength( 5.0 );
    entry.constraintData = dpData;
    entry.wasEdited = true;

    DRC_RULE_SAVER saver;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
    wxString result = saver.GenerateRulesText( entries, nullptr );

    BOOST_CHECK( result.Contains( "DiffPairTest" ) );
    BOOST_CHECK( result.Contains( "track_width" ) );
    BOOST_CHECK( result.Contains( "diff_pair_gap" ) );
}


// ============================================================================
// Integration Tests (Phase 5.2)
// ============================================================================

BOOST_AUTO_TEST_CASE( IntegrationLoadHoleClearanceRules )
{
    // Test: Load issue6879.kicad_dru which has hole_clearance and hole constraints
    wxString ruleText =
        "(version 1)\n"
        "(rule \"PTH to Track Clearance\"\n"
        "    (constraint hole_clearance (min 1.0mm))\n"
        "    (condition \"A.Type == 'Pad' && A.Pad_Type == 'Through-hole' && B.Type =='Track'\"))\n"
        "(rule \"Max Drill Hole Size Mechanical\"\n"
        "    (constraint hole (max 2.0mm))\n"
        "    (condition \"A.Type == 'Pad'\"))";

    DRC_RULE_LOADER loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );

    BOOST_REQUIRE_GE( entries.size(), 2 );

    // First rule should be COPPER_TO_HOLE_CLEARANCE
    BOOST_CHECK_EQUAL( entries[0].panelType, COPPER_TO_HOLE_CLEARANCE );
    BOOST_CHECK_EQUAL( entries[0].ruleName, "PTH to Track Clearance" );
    BOOST_CHECK( !entries[0].condition.IsEmpty() );

    // Second rule should be HOLE_SIZE
    BOOST_CHECK_EQUAL( entries[1].panelType, HOLE_SIZE );
    BOOST_CHECK_EQUAL( entries[1].ruleName, "Max Drill Hole Size Mechanical" );
}

BOOST_AUTO_TEST_CASE( IntegrationLoadEdgeClearanceWithSeverity )
{
    // Test: Load severities.kicad_dru which has severity clause
    wxString ruleText =
        "(version 1)\n"
        "(rule board_edge\n"
        "    (constraint edge_clearance (min 1mm))\n"
        "    (condition \"A.memberOf('board_edge')\")\n"
        "    (severity ignore))";

    DRC_RULE_LOADER loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );

    BOOST_REQUIRE_EQUAL( entries.size(), 1 );
    BOOST_CHECK_EQUAL( entries[0].panelType, COPPER_TO_EDGE_CLEARANCE );
    BOOST_CHECK_EQUAL( entries[0].ruleName, "board_edge" );
    BOOST_CHECK_EQUAL( entries[0].severity, RPT_SEVERITY_IGNORE );
}

BOOST_AUTO_TEST_CASE( IntegrationLoadConnectionWidthRules )
{
    // Test: Load connection_width_rules.kicad_dru
    wxString ruleText =
        "(version 1)\n"
        "(rule high_current_netclass\n"
        "    (constraint connection_width (min 0.16mm))\n"
        "    (condition \"A.NetClass == 'High_current'\"))\n"
        "(rule high_current_area\n"
        "    (constraint connection_width (min 0.16mm))\n"
        "    (condition \"A.insideArea('high_current')\"))";

    DRC_RULE_LOADER loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );

    BOOST_REQUIRE_EQUAL( entries.size(), 2 );

    // Both should be MINIMUM_CONNECTION_WIDTH
    BOOST_CHECK_EQUAL( entries[0].panelType, MINIMUM_CONNECTION_WIDTH );
    BOOST_CHECK_EQUAL( entries[1].panelType, MINIMUM_CONNECTION_WIDTH );

    // Both should have conditions
    BOOST_CHECK( !entries[0].condition.IsEmpty() );
    BOOST_CHECK( !entries[1].condition.IsEmpty() );
}

BOOST_AUTO_TEST_CASE( IntegrationSaveLoadRoundTripMultipleRules )
{
    // Test: Load multiple rules, save, reload - verify equivalent data
    // Use rule names without spaces to avoid sanitization differences
    wxString originalText =
        "(version 1)\n"
        "(rule ClearanceRule\n"
        "    (constraint clearance (min 0.2mm))\n"
        "    (condition \"A.NetClass == 'Power'\"))\n"
        "(rule TrackWidthRule\n"
        "    (constraint track_width (min 0.15mm) (opt 0.2mm) (max 0.3mm)))";

    DRC_RULE_LOADER loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( originalText );

    BOOST_REQUIRE_EQUAL( entries.size(), 2 );

    // Mark as edited to force regeneration
    for( auto& entry : entries )
        entry.wasEdited = true;

    // Save
    DRC_RULE_SAVER saver;
    wxString savedText = saver.GenerateRulesText( entries, nullptr );

    // Reload
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> reloadedEntries = loader.LoadFromString( savedText );

    BOOST_REQUIRE_EQUAL( reloadedEntries.size(), 2 );

    // Verify panel types preserved correctly
    BOOST_CHECK_EQUAL( reloadedEntries[0].panelType, entries[0].panelType );
    BOOST_CHECK_EQUAL( reloadedEntries[1].panelType, entries[1].panelType );

    // Verify rule names preserved
    BOOST_CHECK_EQUAL( reloadedEntries[0].ruleName, "ClearanceRule" );
    BOOST_CHECK_EQUAL( reloadedEntries[1].ruleName, "TrackWidthRule" );
}

BOOST_AUTO_TEST_CASE( IntegrationSaveLoadViaStyleRoundTrip )
{
    // Test: Via style rules round-trip correctly
    wxString originalText =
        "(version 1)\n"
        "(rule \"Via Style\"\n"
        "    (constraint via_diameter (min 0.5mm) (opt 0.6mm) (max 0.8mm))\n"
        "    (constraint hole_size (min 0.2mm) (opt 0.3mm) (max 0.4mm)))";

    DRC_RULE_LOADER loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( originalText );

    BOOST_REQUIRE_EQUAL( entries.size(), 1 );
    BOOST_CHECK_EQUAL( entries[0].panelType, VIA_STYLE );

    auto viaData = std::dynamic_pointer_cast<DRC_RE_VIA_STYLE_CONSTRAINT_DATA>( entries[0].constraintData );
    BOOST_REQUIRE( viaData );

    double originalMinDia = viaData->GetMinViaDiameter();
    double originalMinHole = viaData->GetMinViaHoleSize();

    // Mark as edited and save
    entries[0].wasEdited = true;
    DRC_RULE_SAVER saver;
    wxString savedText = saver.GenerateRulesText( entries, nullptr );

    // Reload and verify values preserved
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> reloadedEntries = loader.LoadFromString( savedText );

    BOOST_REQUIRE_EQUAL( reloadedEntries.size(), 1 );
    BOOST_CHECK_EQUAL( reloadedEntries[0].panelType, VIA_STYLE );

    auto reloadedViaData =
            std::dynamic_pointer_cast<DRC_RE_VIA_STYLE_CONSTRAINT_DATA>( reloadedEntries[0].constraintData );
    BOOST_REQUIRE( reloadedViaData );

    BOOST_CHECK_CLOSE( reloadedViaData->GetMinViaDiameter(), originalMinDia, 0.0001 );
    BOOST_CHECK_CLOSE( reloadedViaData->GetMinViaHoleSize(), originalMinHole, 0.0001 );
}

BOOST_AUTO_TEST_CASE( IntegrationSplitRulePreservesOrder )
{
    // Test: A rule with multiple constraint types splits and preserves order
    wxString ruleText =
        "(version 1)\n"
        "(rule \"Rule A\"\n"
        "    (constraint clearance (min 0.2mm)))\n"
        "(rule \"Rule B\"\n"
        "    (constraint via_diameter (min 0.5mm))\n"
        "    (constraint hole_size (min 0.2mm))\n"
        "    (constraint track_width (min 0.15mm)))\n"
        "(rule \"Rule C\"\n"
        "    (constraint edge_clearance (min 0.3mm)))";

    DRC_RULE_LOADER loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );

    // Rule B splits into VIA_STYLE and ROUTING_WIDTH
    BOOST_REQUIRE_GE( entries.size(), 4 );

    // Find entries by rule name
    int ruleACount = 0, ruleBCount = 0, ruleCCount = 0;

    for( const auto& entry : entries )
    {
        if( entry.ruleName == "Rule A" )
            ruleACount++;
        else if( entry.ruleName == "Rule B" )
            ruleBCount++;
        else if( entry.ruleName == "Rule C" )
            ruleCCount++;
    }

    BOOST_CHECK_EQUAL( ruleACount, 1 );
    BOOST_CHECK_GE( ruleBCount, 2 );
    BOOST_CHECK_EQUAL( ruleCCount, 1 );

    // Verify original order: all Rule A entries before Rule B, Rule B before Rule C
    int lastAIndex = -1, firstBIndex = entries.size(), lastBIndex = -1, firstCIndex = entries.size();

    for( size_t i = 0; i < entries.size(); i++ )
    {
        if( entries[i].ruleName == "Rule A" )
            lastAIndex = i;
        else if( entries[i].ruleName == "Rule B" )
        {
            if( (int)i < firstBIndex )
                firstBIndex = i;

            lastBIndex = i;
        }
        else if( entries[i].ruleName == "Rule C" )
        {
            if( (int)i < firstCIndex )
                firstCIndex = i;
        }
    }

    BOOST_CHECK( lastAIndex < firstBIndex );
    BOOST_CHECK( lastBIndex < firstCIndex );
}


// ============================================================================
// Condition Group Panel Tests
// ============================================================================

BOOST_AUTO_TEST_CASE( ConditionGroupBuildSingleCondition )
{
    // Test: Single condition builds correctly
    // This tests the expression building logic used by DRC_RE_CONDITION_GROUP_PANEL
    wxString expr = "A.NetName == 'VCC'";

    // When parsed and rebuilt, the result should be equivalent
    BOOST_CHECK( !expr.IsEmpty() );
    BOOST_CHECK( expr.Contains( "NetName" ) );
    BOOST_CHECK( expr.Contains( "VCC" ) );
}

BOOST_AUTO_TEST_CASE( ConditionGroupTokenizeAndOperator )
{
    // Test: Tokenizing conditions with AND operator
    wxString expr = "A.NetName == 'VCC' && B.NetClass == 'Power'";

    // Verify the expression contains expected operators
    BOOST_CHECK( expr.Contains( "&&" ) );
    BOOST_CHECK( expr.Contains( "NetName" ) );
    BOOST_CHECK( expr.Contains( "NetClass" ) );
}

BOOST_AUTO_TEST_CASE( ConditionGroupTokenizeOrOperator )
{
    // Test: Tokenizing conditions with OR operator
    wxString expr = "A.intersectsArea('underFPGA') || A.intersectsArea('underDDR')";

    // Verify the expression contains expected operators
    BOOST_CHECK( expr.Contains( "||" ) );
    BOOST_CHECK( expr.Contains( "intersectsArea" ) );
}

BOOST_AUTO_TEST_CASE( ConditionGroupTokenizeAndNotOperator )
{
    // Test: Tokenizing conditions with AND NOT operator
    wxString expr = "A.NetClass == 'Power' && !A.intersectsArea('NoRouting')";

    // Verify the expression contains expected operators and negation
    BOOST_CHECK( expr.Contains( "&&" ) );
    BOOST_CHECK( expr.Contains( "!" ) );
}

BOOST_AUTO_TEST_CASE( ConditionGroupTokenizeComplexExpression )
{
    // Test: Complex expression from vme-wren.kicad_dru
    wxString expr = "A.intersectsArea('underFPGA') || A.intersectsArea('underDDR')";

    // The expression should contain the expected patterns
    BOOST_CHECK( expr.Contains( "||" ) );
    BOOST_CHECK( expr.Contains( "underFPGA" ) );
    BOOST_CHECK( expr.Contains( "underDDR" ) );

    // Count number of conditions (2 in this case)
    size_t orCount = 0;
    size_t pos = 0;

    while( ( pos = expr.find( "||", pos ) ) != wxString::npos )
    {
        orCount++;
        pos += 2;
    }

    BOOST_CHECK_EQUAL( orCount, 1 );
}


// ============================================================================
// DRC Engine Item Filtering Tests
// ============================================================================

BOOST_AUTO_TEST_CASE( ItemFilterExcludesNetInfoAndGenerator )
{
    // Test: Verify the item type filter logic matches expected exclusions
    // This is a unit test for the switch statement in GetItemsMatchingCondition

    std::vector<KICAD_T> excludedTypes = { PCB_NETINFO_T, PCB_GENERATOR_T, PCB_GROUP_T };
    std::vector<KICAD_T> includedTypes = { PCB_TRACE_T, PCB_VIA_T, PCB_PAD_T, PCB_FOOTPRINT_T,
                                            PCB_ZONE_T, PCB_SHAPE_T, PCB_TEXT_T };

    for( KICAD_T type : excludedTypes )
    {
        switch( type )
        {
        case PCB_NETINFO_T:
        case PCB_GENERATOR_T:
        case PCB_GROUP_T:
            BOOST_CHECK( true );  // These should be excluded (continue in real code)
            break;

        default:
            BOOST_FAIL( "Excluded type not handled in filter" );
            break;
        }
    }

    for( KICAD_T type : includedTypes )
    {
        switch( type )
        {
        case PCB_NETINFO_T:
        case PCB_GENERATOR_T:
        case PCB_GROUP_T:
            BOOST_FAIL( "Included type incorrectly matched by filter" );
            break;

        default:
            BOOST_CHECK( true );  // These should be included
            break;
        }
    }
}

BOOST_AUTO_TEST_CASE( RuleSaverSilkToSoldermaskWithCondition )
{
    // Test: Silk-to-soldermask merges layer condition with user condition
    // into a single (condition ...) clause, not two separate ones.
    DRC_RE_LOADED_PANEL_ENTRY entry;
    entry.panelType = SILK_TO_SOLDERMASK_CLEARANCE;
    entry.ruleName = "SilkMaskTest";
    entry.condition = "A.NetClass == 'Power'";
    entry.wasEdited = true;
    entry.layerCondition = LSET( { F_SilkS } );

    auto data = std::make_shared<DRC_RE_SILK_TO_SOLDERMASK_CLEARANCE_CONSTRAINT_DATA>();
    data->SetRuleName( "SilkMaskTest" );
    data->SetConstraintCode( "silk_clearance" );
    data->SetRuleCondition( "A.NetClass == 'Power'" );
    data->SetNumericInputValue( 0.15 );
    entry.constraintData = data;

    DRC_RULE_SAVER saver;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
    wxString result = saver.GenerateRulesText( entries, nullptr );

    // Should have a single merged condition, not two (condition ...) lines
    BOOST_CHECK( result.Contains( "L == 'F.Mask'" ) );
    BOOST_CHECK( result.Contains( "Power" ) );
    BOOST_CHECK( !result.Contains( "(layer" ) );

    // Count occurrences of "(condition" — must be exactly one
    int condCount = 0;
    size_t pos = 0;
    while( ( pos = result.find( "(condition", pos ) ) != wxString::npos )
    {
        condCount++;
        pos++;
    }
    BOOST_CHECK_EQUAL( condCount, 1 );
}

BOOST_AUTO_TEST_CASE( RuleSaverSilkToSoldermaskNoExtraCondition )
{
    // Test: Silk-to-soldermask without extra condition still produces
    // a condition clause, not a (layer ...) clause.
    DRC_RE_LOADED_PANEL_ENTRY entry;
    entry.panelType = SILK_TO_SOLDERMASK_CLEARANCE;
    entry.ruleName = "SilkMaskSimple";
    entry.wasEdited = true;
    entry.layerCondition = LSET( { B_SilkS } );

    auto data = std::make_shared<DRC_RE_SILK_TO_SOLDERMASK_CLEARANCE_CONSTRAINT_DATA>();
    data->SetRuleName( "SilkMaskSimple" );
    data->SetConstraintCode( "silk_clearance" );
    data->SetNumericInputValue( 0.1 );
    entry.constraintData = data;

    DRC_RULE_SAVER saver;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
    wxString result = saver.GenerateRulesText( entries, nullptr );

    BOOST_CHECK( result.Contains( "L == 'B.Mask'" ) );
    BOOST_CHECK( result.Contains( "(condition" ) );
    BOOST_CHECK( !result.Contains( "(layer" ) );
}

BOOST_AUTO_TEST_CASE( RuleSaverSilkToSilkBothLayers )
{
    // Silk-to-silk with both layers generates L == 'F.SilkS' || L == 'B.SilkS'
    DRC_RE_LOADED_PANEL_ENTRY entry;
    entry.panelType = SILK_TO_SILK_CLEARANCE;
    entry.ruleName = "SilkSilkBoth";
    entry.wasEdited = true;
    entry.layerCondition = LSET( { F_SilkS, B_SilkS } );

    auto data = std::make_shared<DRC_RE_SILK_TO_SILK_CLEARANCE_CONSTRAINT_DATA>();
    data->SetRuleName( "SilkSilkBoth" );
    data->SetConstraintCode( "silk_clearance" );
    data->SetNumericInputValue( 0.2 );
    entry.constraintData = data;

    DRC_RULE_SAVER                         saver;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
    wxString                               result = saver.GenerateRulesText( entries, nullptr );

    BOOST_CHECK( result.Contains( "L == 'F.SilkS' || L == 'B.SilkS'" ) );
    BOOST_CHECK( result.Contains( "(condition" ) );
    BOOST_CHECK( !result.Contains( "(layer" ) );
}

BOOST_AUTO_TEST_CASE( RuleSaverSilkToSilkFrontOnly )
{
    // Silk-to-silk front only generates L == 'F.SilkS'
    DRC_RE_LOADED_PANEL_ENTRY entry;
    entry.panelType = SILK_TO_SILK_CLEARANCE;
    entry.ruleName = "SilkSilkFront";
    entry.wasEdited = true;
    entry.layerCondition = LSET( { F_SilkS } );

    auto data = std::make_shared<DRC_RE_SILK_TO_SILK_CLEARANCE_CONSTRAINT_DATA>();
    data->SetRuleName( "SilkSilkFront" );
    data->SetConstraintCode( "silk_clearance" );
    data->SetNumericInputValue( 0.2 );
    entry.constraintData = data;

    DRC_RULE_SAVER                         saver;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
    wxString                               result = saver.GenerateRulesText( entries, nullptr );

    BOOST_CHECK( result.Contains( "L == 'F.SilkS'" ) );
    BOOST_CHECK( !result.Contains( "B.SilkS" ) );
    BOOST_CHECK( result.Contains( "(condition" ) );
    BOOST_CHECK( !result.Contains( "(layer" ) );
}

BOOST_AUTO_TEST_CASE( RuleSaverSilkToSilkWithCondition )
{
    // Silk-to-silk merges layer condition with user condition
    DRC_RE_LOADED_PANEL_ENTRY entry;
    entry.panelType = SILK_TO_SILK_CLEARANCE;
    entry.ruleName = "SilkSilkCond";
    entry.condition = "A.NetClass == 'Signal'";
    entry.wasEdited = true;
    entry.layerCondition = LSET( { F_SilkS } );

    auto data = std::make_shared<DRC_RE_SILK_TO_SILK_CLEARANCE_CONSTRAINT_DATA>();
    data->SetRuleName( "SilkSilkCond" );
    data->SetConstraintCode( "silk_clearance" );
    data->SetRuleCondition( "A.NetClass == 'Signal'" );
    data->SetNumericInputValue( 0.2 );
    entry.constraintData = data;

    DRC_RULE_SAVER                         saver;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
    wxString                               result = saver.GenerateRulesText( entries, nullptr );

    BOOST_CHECK( result.Contains( "L == 'F.SilkS'" ) );
    BOOST_CHECK( result.Contains( "Signal" ) );

    // Single merged condition
    int    condCount = 0;
    size_t pos = 0;
    while( ( pos = result.find( "(condition", pos ) ) != wxString::npos )
    {
        condCount++;
        pos++;
    }
    BOOST_CHECK_EQUAL( condCount, 1 );
}

BOOST_AUTO_TEST_CASE( ValidateViasUnderSmdValid )
{
    // Valid: at least one via type is selected
    DRC_RE_VIAS_UNDER_SMD_CONSTRAINT_DATA data1( 0, 0, "OnlyMicro", false, true, false, false );
    VALIDATION_RESULT                     result1 = data1.Validate();
    BOOST_CHECK( result1.isValid );
    BOOST_CHECK( result1.errors.empty() );

    // Valid: all selected
    DRC_RE_VIAS_UNDER_SMD_CONSTRAINT_DATA data2( 0, 0, "AllVias", true, true, true, true );
    VALIDATION_RESULT                     result2 = data2.Validate();
    BOOST_CHECK( result2.isValid );
    BOOST_CHECK( result2.errors.empty() );
}

BOOST_AUTO_TEST_CASE( ValidateViasUnderSmdInvalid )
{
    // Invalid: no via type selected
    DRC_RE_VIAS_UNDER_SMD_CONSTRAINT_DATA data( 0, 0, "NoneSelected", false, false, false, false );
    VALIDATION_RESULT                     result = data.Validate();
    BOOST_CHECK( !result.isValid );
    BOOST_CHECK( !result.errors.empty() );

    bool foundError = false;
    for( const auto& error : result.errors )
    {
        if( error.find( "At least one via type must be selected" ) != std::string::npos )
            foundError = true;
    }
    BOOST_CHECK( foundError );
}

BOOST_AUTO_TEST_CASE( ViasUnderSmdClauseGeneration )
{
    RULE_GENERATION_CONTEXT ctx;
    ctx.ruleName = "TestRule";
    ctx.constraintCode = "disallow_via";

    // All four selected - shorthand "via" used
    DRC_RE_VIAS_UNDER_SMD_CONSTRAINT_DATA allData( 0, 0, "AllRule", true, true, true, true );
    auto                                  clauses = allData.GetConstraintClauses( ctx );
    BOOST_REQUIRE_EQUAL( clauses.size(), 1 );
    BOOST_CHECK( clauses[0].Contains( "disallow via" ) );
    BOOST_CHECK( !clauses[0].Contains( "through_via" ) );

    // Only micro_via selected - specific token
    DRC_RE_VIAS_UNDER_SMD_CONSTRAINT_DATA microOnly( 0, 0, "MicroRule", false, true, false, false );
    clauses = microOnly.GetConstraintClauses( ctx );
    BOOST_REQUIRE_EQUAL( clauses.size(), 1 );
    BOOST_CHECK( clauses[0].Contains( "micro_via" ) );
    BOOST_CHECK( !clauses[0].Contains( "through_via" ) );
    BOOST_CHECK( !clauses[0].Contains( "blind_via" ) );
    BOOST_CHECK( !clauses[0].Contains( "buried_via" ) );

    // Through + blind selected - both tokens, no shorthand
    DRC_RE_VIAS_UNDER_SMD_CONSTRAINT_DATA twoTypes( 0, 0, "TwoRule", true, false, true, false );
    clauses = twoTypes.GetConstraintClauses( ctx );
    BOOST_REQUIRE_EQUAL( clauses.size(), 1 );
    BOOST_CHECK( clauses[0].Contains( "through_via" ) );
    BOOST_CHECK( clauses[0].Contains( "blind_via" ) );
    BOOST_CHECK( !clauses[0].Contains( "micro_via" ) );
    BOOST_CHECK( !clauses[0].Contains( "buried_via" ) );

    // None selected - empty
    DRC_RE_VIAS_UNDER_SMD_CONSTRAINT_DATA noneData( 0, 0, "NoneRule", false, false, false, false );
    clauses = noneData.GetConstraintClauses( ctx );
    BOOST_CHECK( clauses.empty() );
}

BOOST_AUTO_TEST_CASE( IntegrationViasUnderSmdRoundTrip )
{
    // Load a rule with only micro_via disallowed
    wxString originalText = "(version 1)\n"
                            "(rule \"ViasUnderSmdTest\"\n"
                            "    (constraint disallow micro_via)\n"
                            "    (condition \"A.Pad_Type == 'SMD' && A.insideArea('BGA*')\"))";

    DRC_RULE_LOADER                        loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( originalText );

    BOOST_REQUIRE_EQUAL( entries.size(), 1 );
    BOOST_CHECK_EQUAL( entries[0].panelType, VIAS_UNDER_SMD );

    auto viaData = std::dynamic_pointer_cast<DRC_RE_VIAS_UNDER_SMD_CONSTRAINT_DATA>( entries[0].constraintData );
    BOOST_REQUIRE( viaData );

    // Only micro_via should be checked
    BOOST_CHECK( !viaData->GetDisallowThroughVias() );
    BOOST_CHECK( viaData->GetDisallowMicroVias() );
    BOOST_CHECK( !viaData->GetDisallowBlindVias() );
    BOOST_CHECK( !viaData->GetDisallowBuriedVias() );

    // Mark as edited and save
    entries[0].wasEdited = true;
    DRC_RULE_SAVER saver;
    wxString       savedText = saver.GenerateRulesText( entries, nullptr );

    // Verify saved text preserves specific via type (not blanket "via")
    BOOST_CHECK( savedText.Contains( "micro_via" ) );
    BOOST_CHECK( !savedText.Contains( "through_via" ) );
    BOOST_CHECK( !savedText.Contains( "blind_via" ) );
    BOOST_CHECK( !savedText.Contains( "buried_via" ) );

    // Reload and verify roundtrip
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> reloadedEntries = loader.LoadFromString( savedText );
    BOOST_REQUIRE_EQUAL( reloadedEntries.size(), 1 );

    auto reloadedData =
            std::dynamic_pointer_cast<DRC_RE_VIAS_UNDER_SMD_CONSTRAINT_DATA>( reloadedEntries[0].constraintData );
    BOOST_REQUIRE( reloadedData );

    BOOST_CHECK( !reloadedData->GetDisallowThroughVias() );
    BOOST_CHECK( reloadedData->GetDisallowMicroVias() );
    BOOST_CHECK( !reloadedData->GetDisallowBlindVias() );
    BOOST_CHECK( !reloadedData->GetDisallowBuriedVias() );

    // Test blanket "via" sets all four flags
    wxString blanketText = "(version 1)\n"
                           "(rule \"BlanketVia\"\n"
                           "    (constraint disallow via)\n"
                           "    (condition \"A.Pad_Type == 'SMD' && A.insideArea('BGA*')\"))";

    entries = loader.LoadFromString( blanketText );
    BOOST_REQUIRE_EQUAL( entries.size(), 1 );

    auto blanketData = std::dynamic_pointer_cast<DRC_RE_VIAS_UNDER_SMD_CONSTRAINT_DATA>( entries[0].constraintData );
    BOOST_REQUIRE( blanketData );

    BOOST_CHECK( blanketData->GetDisallowThroughVias() );
    BOOST_CHECK( blanketData->GetDisallowMicroVias() );
    BOOST_CHECK( blanketData->GetDisallowBlindVias() );
    BOOST_CHECK( blanketData->GetDisallowBuriedVias() );

    // Test multiple specific types roundtrip
    wxString multiText = "(version 1)\n"
                         "(rule \"TwoVias\"\n"
                         "    (constraint disallow through_via blind_via)\n"
                         "    (condition \"A.Pad_Type == 'SMD' && A.insideArea('BGA*')\"))";

    entries = loader.LoadFromString( multiText );
    BOOST_REQUIRE_EQUAL( entries.size(), 1 );

    auto multiData = std::dynamic_pointer_cast<DRC_RE_VIAS_UNDER_SMD_CONSTRAINT_DATA>( entries[0].constraintData );
    BOOST_REQUIRE( multiData );

    BOOST_CHECK( multiData->GetDisallowThroughVias() );
    BOOST_CHECK( !multiData->GetDisallowMicroVias() );
    BOOST_CHECK( multiData->GetDisallowBlindVias() );
    BOOST_CHECK( !multiData->GetDisallowBuriedVias() );

    entries[0].wasEdited = true;
    savedText = saver.GenerateRulesText( entries, nullptr );

    BOOST_CHECK( savedText.Contains( "through_via" ) );
    BOOST_CHECK( savedText.Contains( "blind_via" ) );
    BOOST_CHECK( !savedText.Contains( "micro_via" ) );
    BOOST_CHECK( !savedText.Contains( "buried_via" ) );
}

BOOST_AUTO_TEST_CASE( RuleLoaderNonStandardOrientationFallsBackToCustom )
{
    // Quoted name
    wxString ruleText = "(version 1)\n"
                        "(rule \"Angled\"\n"
                        "    (constraint assertion \"A.Orientation == 46 deg\")\n"
                        ")";

    DRC_RULE_LOADER                        loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );

    BOOST_REQUIRE_EQUAL( entries.size(), 1 );
    BOOST_CHECK_EQUAL( entries[0].panelType, CUSTOM_RULE );

    auto customData = std::dynamic_pointer_cast<DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA>( entries[0].constraintData );
    BOOST_REQUIRE( customData );
    BOOST_CHECK( customData->GetRuleText().Contains( wxS( "46 deg" ) ) );
    BOOST_CHECK( !customData->GetRuleText().Contains( wxS( "Angled" ) ) );

    // Unquoted name
    wxString ruleTextUnquoted = "(version 1)\n"
                                "(rule Angled\n"
                                "    (constraint assertion \"A.Orientation == 46 deg\")\n"
                                ")";

    entries = loader.LoadFromString( ruleTextUnquoted );

    BOOST_REQUIRE_EQUAL( entries.size(), 1 );
    BOOST_CHECK_EQUAL( entries[0].panelType, CUSTOM_RULE );

    auto customDataUnquoted =
            std::dynamic_pointer_cast<DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA>( entries[0].constraintData );
    BOOST_REQUIRE( customDataUnquoted );
    BOOST_CHECK( customDataUnquoted->GetRuleText().Contains( wxS( "46 deg" ) ) );
    BOOST_CHECK( !customDataUnquoted->GetRuleText().Contains( wxS( "Angled" ) ) );
}

BOOST_AUTO_TEST_CASE( RuleLoaderStandardOrientationLoadsStructured )
{
    wxString ruleText = "(version 1)\n"
                        "(rule \"Standard\"\n"
                        "    (constraint assertion \"A.Orientation == 0 deg || A.Orientation == 90 deg\")\n"
                        ")";

    DRC_RULE_LOADER                        loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );

    BOOST_REQUIRE_EQUAL( entries.size(), 1 );
    BOOST_CHECK_EQUAL( entries[0].panelType, ALLOWED_ORIENTATION );

    auto orientData =
            std::dynamic_pointer_cast<DRC_RE_ALLOWED_ORIENTATION_CONSTRAINT_DATA>( entries[0].constraintData );
    BOOST_REQUIRE( orientData );
    BOOST_CHECK( orientData->GetIsZeroDegreesAllowed() );
    BOOST_CHECK( orientData->GetIsNinetyDegreesAllowed() );
    BOOST_CHECK( !orientData->GetIsOneEightyDegreesAllowed() );
    BOOST_CHECK( !orientData->GetIsAllDegreesAllowed() );

    // Single-line variant
    wxString singleLine = "(version 1) (rule \"SingleLine\" (constraint assertion \"A.Orientation == 0 deg || "
                          "A.Orientation == 90 deg\"))";

    entries = loader.LoadFromString( singleLine );

    BOOST_REQUIRE_EQUAL( entries.size(), 1 );
    BOOST_CHECK_EQUAL( entries[0].panelType, ALLOWED_ORIENTATION );

    auto singleLineData =
            std::dynamic_pointer_cast<DRC_RE_ALLOWED_ORIENTATION_CONSTRAINT_DATA>( entries[0].constraintData );
    BOOST_REQUIRE( singleLineData );
    BOOST_CHECK( singleLineData->GetIsZeroDegreesAllowed() );
    BOOST_CHECK( singleLineData->GetIsNinetyDegreesAllowed() );
    BOOST_CHECK( !singleLineData->GetIsOneEightyDegreesAllowed() );
    BOOST_CHECK( !singleLineData->GetIsAllDegreesAllowed() );
}

BOOST_AUTO_TEST_CASE( RuleLoaderAllowAllOrientationLoadsStructured )
{
    wxString ruleText = "(version 1)\n"
                        "(rule \"AllAngles\"\n"
                        "    (constraint assertion \"A.Orientation == 0 deg || A.Orientation == 90 deg || "
                        "A.Orientation == 180 deg || A.Orientation == 270 deg\")\n"
                        ")";

    DRC_RULE_LOADER                        loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );

    BOOST_REQUIRE_EQUAL( entries.size(), 1 );
    BOOST_CHECK_EQUAL( entries[0].panelType, ALLOWED_ORIENTATION );

    auto orientData =
            std::dynamic_pointer_cast<DRC_RE_ALLOWED_ORIENTATION_CONSTRAINT_DATA>( entries[0].constraintData );
    BOOST_REQUIRE( orientData );
    BOOST_CHECK( orientData->GetIsAllDegreesAllowed() );
    BOOST_CHECK( orientData->GetIsZeroDegreesAllowed() );
    BOOST_CHECK( orientData->GetIsNinetyDegreesAllowed() );
    BOOST_CHECK( orientData->GetIsOneEightyDegreesAllowed() );
    BOOST_CHECK( orientData->GetIsTwoSeventyDegreesAllowed() );
}

BOOST_AUTO_TEST_CASE( RuleLoaderPermittedLayersInnerLayerFallsBackToCustom )
{
    wxString ruleText = "(version 1)\n"
                        "(rule \"InnerLayers\"\n"
                        "    (constraint assertion \"A.Layer == 'F.Cu' || A.Layer == 'In1.Cu'\")\n"
                        ")";

    DRC_RULE_LOADER                        loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );

    BOOST_REQUIRE_EQUAL( entries.size(), 1 );
    BOOST_CHECK_EQUAL( entries[0].panelType, CUSTOM_RULE );

    auto customData = std::dynamic_pointer_cast<DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA>( entries[0].constraintData );
    BOOST_REQUIRE( customData );
    BOOST_CHECK( customData->GetRuleText().Contains( wxS( "In1.Cu" ) ) );
}

BOOST_AUTO_TEST_CASE( RuleLoaderPermittedLayersStandardLoadsStructured )
{
    wxString ruleText = "(version 1)\n"
                        "(rule \"StandardLayers\"\n"
                        "    (constraint assertion \"A.Layer == 'F.Cu' || A.Layer == 'B.Cu'\")\n"
                        ")";

    DRC_RULE_LOADER                        loader;
    std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );

    BOOST_REQUIRE_EQUAL( entries.size(), 1 );
    BOOST_CHECK_EQUAL( entries[0].panelType, PERMITTED_LAYERS );

    auto layerData = std::dynamic_pointer_cast<DRC_RE_PERMITTED_LAYERS_CONSTRAINT_DATA>( entries[0].constraintData );
    BOOST_REQUIRE( layerData );
    BOOST_CHECK( layerData->GetTopLayerEnabled() );
    BOOST_CHECK( layerData->GetBottomLayerEnabled() );
}

BOOST_AUTO_TEST_SUITE_END()
