/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 */

#define BOOST_TEST_NO_MAIN
#include <boost/test/unit_test.hpp>

#include <tool/grid_helper.h>

void TEST_CLEAR_ANCHORS( GRID_HELPER& helper )
{
    helper.clearAnchors();
}

BOOST_AUTO_TEST_SUITE( GridHelperTest )

BOOST_AUTO_TEST_CASE( DefaultConstructor )
{
    GRID_HELPER helper;

    // Test default state
    BOOST_CHECK( helper.GetSnap() );
    BOOST_CHECK( helper.GetUseGrid() );

    // Test that manual setters work
    helper.SetGridSize( VECTOR2D( 100, 100 ) );
    helper.SetOrigin( VECTOR2I( 50, 50 ) );
    helper.SetGridSnapping( true );

    VECTOR2I grid = helper.GetGrid();
    BOOST_CHECK_EQUAL( grid.x, 100 );
    BOOST_CHECK_EQUAL( grid.y, 100 );

    VECTOR2I origin = helper.GetOrigin();
    BOOST_CHECK_EQUAL( origin.x, 50 );
    BOOST_CHECK_EQUAL( origin.y, 50 );
}

BOOST_AUTO_TEST_CASE( AlignBasic )
{
    GRID_HELPER helper;
    helper.SetGridSize( VECTOR2D( 100, 100 ) );
    helper.SetOrigin( VECTOR2I( 0, 0 ) );
    helper.SetGridSnapping( true );

    // Test basic alignment - should round to nearest grid point
    VECTOR2I aligned = helper.Align( VECTOR2I( 149, 251 ) );
    BOOST_CHECK_EQUAL( aligned.x, 100 );
    BOOST_CHECK_EQUAL( aligned.y, 300 );

    // Test exact grid points
    aligned = helper.Align( VECTOR2I( 200, 300 ) );
    BOOST_CHECK_EQUAL( aligned.x, 200 );
    BOOST_CHECK_EQUAL( aligned.y, 300 );

    // Test negative coordinates
    aligned = helper.Align( VECTOR2I( -149, -251 ) );
    BOOST_CHECK_EQUAL( aligned.x, -100 );
    BOOST_CHECK_EQUAL( aligned.y, -300 );
}

BOOST_AUTO_TEST_CASE( AlignGridWithCustomGrid )
{
    GRID_HELPER helper;
    helper.SetGridSize( VECTOR2D( 50, 50 ) );
    helper.SetOrigin( VECTOR2I( 0, 0 ) );
    helper.SetGridSnapping( true );

    VECTOR2I aligned = helper.AlignGrid( VECTOR2I( 26, 74 ) );
    BOOST_CHECK_EQUAL( aligned.x, 50 );
    BOOST_CHECK_EQUAL( aligned.y, 50 );

    // Test AlignGrid with specific grid parameter
    aligned = helper.AlignGrid( VECTOR2I( 26, 74 ), VECTOR2D( 25, 25 ), VECTOR2D( 0, 0 ) );
    BOOST_CHECK_EQUAL( aligned.x, 25 );
    BOOST_CHECK_EQUAL( aligned.y, 75 );
}

BOOST_AUTO_TEST_CASE( AlignWithOriginOffset )
{
    GRID_HELPER helper;
    helper.SetGridSize( VECTOR2D( 100, 100 ) );
    helper.SetOrigin( VECTOR2I( 25, 25 ) );
    helper.SetGridSnapping( true );

    // When grid has an origin offset, alignment should work from the new reference point
    VECTOR2I aligned = helper.AlignGrid( VECTOR2I( 149, 251 ) );
    BOOST_CHECK_EQUAL( aligned.x, 125 );
    BOOST_CHECK_EQUAL( aligned.y, 225 );
}

BOOST_AUTO_TEST_CASE( AlignWithAuxiliaryAxes )
{
    GRID_HELPER helper;
    helper.SetGridSize( VECTOR2D( 100, 100 ) );
    helper.SetOrigin( VECTOR2I( 0, 0 ) );
    helper.SetGridSnapping( true );

    // Set auxiliary axis at (75, 75)
    helper.SetAuxAxes( true, VECTOR2I( 75, 75 ) );

    // Point closer to aux axis than grid should snap to aux axis
    VECTOR2I aligned = helper.Align( VECTOR2I( 80, 80 ) );
    BOOST_CHECK_EQUAL( aligned.x, 75 );  // Closer to aux axis X
    BOOST_CHECK_EQUAL( aligned.y, 75 );  // Closer to aux axis Y

    // Point closer to grid than aux axis should snap to grid
    aligned = helper.Align( VECTOR2I( 95, 95 ) );
    BOOST_CHECK_EQUAL( aligned.x, 100 );  // Closer to grid
    BOOST_CHECK_EQUAL( aligned.y, 100 );  // Closer to grid

    // Disable aux axes
    helper.SetAuxAxes( false );
    aligned = helper.Align( VECTOR2I( 80, 80 ) );
    BOOST_CHECK_EQUAL( aligned.x, 100 );  // Should snap to grid only
    BOOST_CHECK_EQUAL( aligned.y, 100 );
}

BOOST_AUTO_TEST_CASE( GridSnappingDisabled )
{
    GRID_HELPER helper;
    helper.SetGridSize( VECTOR2D( 100, 100 ) );
    helper.SetOrigin( VECTOR2I( 0, 0 ) );
    helper.SetGridSnapping( false );  // Disable grid snapping

    // When grid snapping is disabled, Align should return original point
    VECTOR2I original( 149, 251 );
    VECTOR2I aligned = helper.Align( original );
    BOOST_CHECK_EQUAL( aligned.x, original.x );
    BOOST_CHECK_EQUAL( aligned.y, original.y );

    // AlignGrid should still work regardless of grid snapping setting
    aligned = helper.AlignGrid( original );
    BOOST_CHECK_EQUAL( aligned.x, 100 );
    BOOST_CHECK_EQUAL( aligned.y, 300 );
}

BOOST_AUTO_TEST_CASE( UseGridDisabled )
{
    GRID_HELPER helper;
    helper.SetGridSize( VECTOR2D( 100, 100 ) );
    helper.SetOrigin( VECTOR2I( 0, 0 ) );
    helper.SetGridSnapping( true );
    helper.SetUseGrid( false );  // Disable grid usage

    // When grid usage is disabled, Align should return original point
    VECTOR2I original( 149, 251 );
    VECTOR2I aligned = helper.Align( original );
    BOOST_CHECK_EQUAL( aligned.x, original.x );
    BOOST_CHECK_EQUAL( aligned.y, original.y );
}

BOOST_AUTO_TEST_CASE( AsymmetricGrid )
{
    GRID_HELPER helper;
    helper.SetGridSize( VECTOR2D( 25, 75 ) );  // Different X and Y grid sizes
    helper.SetOrigin( VECTOR2I( 0, 0 ) );
    helper.SetGridSnapping( true );

    VECTOR2I aligned = helper.Align( VECTOR2I( 30, 100 ) );
    BOOST_CHECK_EQUAL( aligned.x, 25 );   // Nearest 25-unit boundary
    BOOST_CHECK_EQUAL( aligned.y, 75 );   // Nearest 75-unit boundary

    aligned = helper.Align( VECTOR2I( 40, 120 ) );
    BOOST_CHECK_EQUAL( aligned.x, 50 );   // Next 25-unit boundary
    BOOST_CHECK_EQUAL( aligned.y, 150 );  // Next 75-unit boundary
}

BOOST_AUTO_TEST_CASE( SnapFlags )
{
    GRID_HELPER helper;

    // Test snap flag getters/setters
    BOOST_CHECK( helper.GetSnap() );  // Default should be true

    helper.SetSnap( false );
    BOOST_CHECK( !helper.GetSnap() );

    helper.SetSnap( true );
    BOOST_CHECK( helper.GetSnap() );

    // Test grid usage flag
    BOOST_CHECK( helper.GetUseGrid() );  // Default should be true

    helper.SetUseGrid( false );
    BOOST_CHECK( !helper.GetUseGrid() );

    helper.SetUseGrid( true );
    BOOST_CHECK( helper.GetUseGrid() );
}

BOOST_AUTO_TEST_CASE( MaskOperations )
{
    GRID_HELPER helper;

    // Test mask operations
    helper.SetMask( GRID_HELPER::CORNER | GRID_HELPER::OUTLINE );
    helper.SetMaskFlag( GRID_HELPER::SNAPPABLE );
    helper.ClearMaskFlag( GRID_HELPER::CORNER );

    // These don't have getters, so we can't verify the mask state directly
    // but we can verify the methods don't crash
}

BOOST_AUTO_TEST_CASE( SkipPoint )
{
    GRID_HELPER helper;

    // Test skip point operations
    helper.SetSkipPoint( VECTOR2I( 100, 100 ) );
    helper.ClearSkipPoint();

    // These methods should not crash
}

BOOST_AUTO_TEST_CASE( GridTypeAlignment )
{
    GRID_HELPER helper;
    helper.SetGridSize( VECTOR2D( 100, 100 ) );
    helper.SetOrigin( VECTOR2I( 0, 0 ) );
    helper.SetGridSnapping( true );

    // Test alignment with specific grid type
    VECTOR2I aligned = helper.Align( VECTOR2I( 149, 251 ), GRID_CURRENT );
    BOOST_CHECK_EQUAL( aligned.x, 100 );
    BOOST_CHECK_EQUAL( aligned.y, 300 );

    aligned = helper.AlignGrid( VECTOR2I( 149, 251 ), GRID_CURRENT );
    BOOST_CHECK_EQUAL( aligned.x, 100 );
    BOOST_CHECK_EQUAL( aligned.y, 300 );
}

BOOST_AUTO_TEST_CASE( EdgeCases )
{
    GRID_HELPER helper;
    helper.SetGridSize( VECTOR2D( 1, 1 ) );  // Very small grid
    helper.SetOrigin( VECTOR2I( 0, 0 ) );
    helper.SetGridSnapping( true );

    // Test with very small grid
    VECTOR2I aligned = helper.Align( VECTOR2I( 5, 5 ) );
    BOOST_CHECK_EQUAL( aligned.x, 5 );
    BOOST_CHECK_EQUAL( aligned.y, 5 );

    // Test with zero point
    aligned = helper.Align( VECTOR2I( 0, 0 ) );
    BOOST_CHECK_EQUAL( aligned.x, 0 );
    BOOST_CHECK_EQUAL( aligned.y, 0 );

    // Test with large grid
    helper.SetGridSize( VECTOR2D( 10000, 10000 ) );
    aligned = helper.Align( VECTOR2I( 3000, 7000 ) );
    BOOST_CHECK_EQUAL( aligned.x, 0 );     // Closer to 0 than 10000
    BOOST_CHECK_EQUAL( aligned.y, 10000 ); // Closer to 10000 than 0
}

BOOST_AUTO_TEST_CASE( GetGridSize )
{
    GRID_HELPER helper;

    // Test GetGridSize with different grid types
    helper.SetGridSize( VECTOR2D( 50, 75 ) );

    VECTOR2D gridSize = helper.GetGridSize( GRID_CURRENT );
    BOOST_CHECK_EQUAL( gridSize.x, 50 );
    BOOST_CHECK_EQUAL( gridSize.y, 75 );

    // Other grid types should return the same in the base implementation
    gridSize = helper.GetGridSize( GRID_CONNECTABLE );
    BOOST_CHECK_EQUAL( gridSize.x, 50 );
    BOOST_CHECK_EQUAL( gridSize.y, 75 );
}

BOOST_AUTO_TEST_CASE( VisibleGrid )
{
    GRID_HELPER helper;
    helper.SetVisibleGridSize( VECTOR2D( 25, 35 ) );

    VECTOR2D visibleGrid = helper.GetVisibleGrid();
    BOOST_CHECK_EQUAL( visibleGrid.x, 25 );
    BOOST_CHECK_EQUAL( visibleGrid.y, 35 );
}

BOOST_AUTO_TEST_CASE( SnapPointManagement )
{
    GRID_HELPER helper;

    // Initially should have no snapped point
    auto snappedPoint = helper.GetSnappedPoint();
    BOOST_CHECK( !snappedPoint.has_value() );

    // After clearing anchors, still no snapped point
    TEST_CLEAR_ANCHORS( helper );
    snappedPoint = helper.GetSnappedPoint();
    BOOST_CHECK( !snappedPoint.has_value() );
}


BOOST_AUTO_TEST_CASE( AlignGridWithNonPageOrigin )
{
    // Issue #21800: Grid snapping should produce positions that are exact multiples
    // of the grid size relative to the grid origin, regardless of display origin setting.
    // When grid sizes go through VECTOR2D, floating-point imprecision in the
    // VECTOR2D -> VECTOR2I truncation could produce incorrect grid positions.

    GRID_HELPER helper;
    helper.SetGridSnapping( true );

    // PCB IU_PER_MM = 1000000 (nanometers)
    constexpr int IU_PER_MM = 1000000;

    struct TestCase
    {
        const char* name;
        double      gridSizeMM;
        int         gridOriginX;
        int         gridOriginY;
        int         pointX;
        int         pointY;
        int         expectedX;
        int         expectedY;
    };

    // Test various grid sizes, including ones that don't convert exactly from mm to nm
    std::vector<TestCase> cases = {
        // 1mm grid with non-zero origin
        { "1mm grid, origin at 47.3mm",
          1.0, 47300000, 25300000,
          127400000, 95400000,
          0, 0 },

        // 0.5mm grid
        { "0.5mm grid, origin at 47.3mm",
          0.5, 47300000, 25300000,
          127400000, 95400000,
          0, 0 },

        // 0.1mm grid (0.1 is NOT exact in IEEE 754 double)
        { "0.1mm grid, origin at 47.3mm",
          0.1, 47300000, 25300000,
          127340000, 95340000,
          0, 0 },

        // 25 mil = 0.635mm (0.635 is NOT exact in IEEE 754 double)
        { "25mil grid, origin at 47.625mm",
          0.635, 47625000, 25400000,
          127960000, 95250000,
          0, 0 },

        // 10 mil = 0.254mm (0.254 is NOT exact in IEEE 754 double)
        { "10mil grid, origin at 47.752mm",
          0.254, 47752000, 25400000,
          127960000, 95250000,
          0, 0 },

        // 50 mil = 1.27mm
        { "50mil grid, origin at 47.625mm",
          1.27, 47625000, 25400000,
          127960000, 95250000,
          0, 0 },
    };

    // Compute all expected values using integer grid size (the ground truth)
    for( auto& tc : cases )
    {
        int gridSizeIU = KiROUND( tc.gridSizeMM * IU_PER_MM );

        tc.expectedX = KiROUND( double( tc.pointX - tc.gridOriginX ) / gridSizeIU )
                       * gridSizeIU + tc.gridOriginX;
        tc.expectedY = KiROUND( double( tc.pointY - tc.gridOriginY ) / gridSizeIU )
                       * gridSizeIU + tc.gridOriginY;
    }

    for( const auto& tc : cases )
    {
        BOOST_TEST_CONTEXT( tc.name )
        {
            int gridSizeIU = KiROUND( tc.gridSizeMM * IU_PER_MM );

            // Simulate the grid size coming through VECTOR2D (as it does from GAL)
            VECTOR2D gridD( tc.gridSizeMM * IU_PER_MM, tc.gridSizeMM * IU_PER_MM );
            VECTOR2D offsetD( tc.gridOriginX, tc.gridOriginY );

            helper.SetGridSize( gridD );
            helper.SetOrigin( VECTOR2I( tc.gridOriginX, tc.gridOriginY ) );

            VECTOR2I point( tc.pointX, tc.pointY );

            // Test AlignGrid with VECTOR2D parameters (the path that goes through
            // implicit VECTOR2D -> VECTOR2I truncation in computeNearest)
            VECTOR2I resultD = helper.AlignGrid( point, gridD, offsetD );

            // Test AlignGrid with no parameters (uses GetGrid() which rounds properly)
            VECTOR2I resultI = helper.AlignGrid( point );

            BOOST_CHECK_EQUAL( resultD.x, tc.expectedX );
            BOOST_CHECK_EQUAL( resultD.y, tc.expectedY );
            BOOST_CHECK_EQUAL( resultI.x, tc.expectedX );
            BOOST_CHECK_EQUAL( resultI.y, tc.expectedY );

            // Verify that the result is on-grid
            BOOST_CHECK_EQUAL( ( resultD.x - tc.gridOriginX ) % gridSizeIU, 0 );
            BOOST_CHECK_EQUAL( ( resultD.y - tc.gridOriginY ) % gridSizeIU, 0 );
        }
    }
}


BOOST_AUTO_TEST_CASE( MovementFromOffGridAnchor )
{
    // Issue #23308 / #21800: When moving a footprint by its anchor, the reference point
    // should be the actual anchor (not grid-snapped). The final position must still land
    // exactly on-grid because AlignGrid now rounds VECTOR2D grid parameters correctly.

    GRID_HELPER helper;
    helper.SetGridSnapping( true );

    constexpr int IU_PER_MM = 1000000;

    struct TestCase
    {
        const char* name;
        double      gridSizeMM;
        int         anchorX;
        int         anchorY;
        int         mouseTargetX;
        int         mouseTargetY;
    };

    std::vector<TestCase> cases = {
        // Anchor at 0.05mm on a 0.1mm grid
        { "0.1mm grid, anchor at half-grid",
          0.1, 50000, 50000, 300000, 300000 },

        // Anchor at 0.127mm on a 0.254mm (10 mil) grid
        { "10mil grid, anchor at half-grid",
          0.254, 127000, 127000, 762000, 508000 },

        // Anchor at 0.3175mm on a 0.635mm (25 mil) grid
        { "25mil grid, anchor at half-grid",
          0.635, 317500, 317500, 1905000, 1270000 },
    };

    for( const auto& tc : cases )
    {
        BOOST_TEST_CONTEXT( tc.name )
        {
            int gridSizeIU = KiROUND( tc.gridSizeMM * IU_PER_MM );

            VECTOR2D gridD( tc.gridSizeMM * IU_PER_MM, tc.gridSizeMM * IU_PER_MM );
            VECTOR2D offsetD( 0, 0 );

            // Simulate the cursor snapping to grid at the target mouse position. This is
            // what BestSnapAnchor does during the move loop.
            VECTOR2I snappedTarget = helper.AlignGrid( VECTOR2I( tc.mouseTargetX, tc.mouseTargetY ),
                                                       gridD, offsetD );

            // The movement delta starts from the actual anchor (off-grid), not a grid-snapped ref
            VECTOR2I anchor( tc.anchorX, tc.anchorY );
            VECTOR2I movement = snappedTarget - anchor;
            VECTOR2I finalPos = anchor + movement;

            // Final position must be exactly on-grid
            BOOST_CHECK_EQUAL( finalPos.x % gridSizeIU, 0 );
            BOOST_CHECK_EQUAL( finalPos.y % gridSizeIU, 0 );

            // And it must equal the snapped target exactly
            BOOST_CHECK_EQUAL( finalPos.x, snappedTarget.x );
            BOOST_CHECK_EQUAL( finalPos.y, snappedTarget.y );
        }
    }
}


BOOST_AUTO_TEST_SUITE_END()