unit Test.Myc.Ast.RTL;

interface

uses
    DUnitX.TestFramework,
    System.SysUtils,
    System.Generics.Collections,
    System.Math,
    System.DateUtils,
    Myc.Data.Scalar,
    Myc.Data.Value,
    Myc.Ast.Types,
    Myc.Ast.Scope,
    Myc.Ast.RTL,
    Myc.Ast.RTL.Core;

type
    [TestFixture]
    TTestMycAstRTL = class
    private
        FScope: IExecutionScope;

        function Call(const Name: string; const Args: array of TDataValue): TDataValue;
        function ValI(V: Int64): TDataValue;
        function ValF(V: Double): TDataValue;
    public
        [Setup]
        procedure Setup;

        // --- Registration ---
        [Test]
        [IgnoreMemoryLeaks]
        procedure RTL_Registration_SymbolsArePresent;

        // --- Floating Point Math ---
        [Test]
        [TestCase('Add_FF', '+,1.5,2.5,4.0')]
        [TestCase('Div_FF', '/,10,2,5.0')]
        procedure RTL_Math_Float(const Op: string; A, B, Expected: Double);

        // --- Integer Math (New) ---
        [Test]
        [TestCase('Div_Int', 'div,10,3,3')]
        [TestCase('Mod_Int', 'mod,10,3,1')]
        procedure RTL_Math_Integer(const Op: string; A, B, Expected: Int64);

        // --- Bitwise Operations (New) ---
        [Test]
        [TestCase('BitAnd', 'and,3,2,2')] // 011 & 010 = 010 (2)
        [TestCase('BitOr', 'or,1,2,3')] // 001 | 010 = 011 (3)
        [TestCase('BitXor', 'xor,3,1,2')] // 011 ^ 001 = 010 (2)
        [TestCase('Shl', 'shl,1,2,4')] // 1 << 2 = 4
        [TestCase('Shr', 'shr,4,1,2')] // 4 >> 1 = 2
        procedure RTL_Bitwise(const Op: string; A, B, Expected: Int64);

        // --- Rounding (New) ---
        [Test]
        [TestCase('Round_Up', '3.6,4')]
        [TestCase('Round_Down', '3.4,3')]
        [TestCase('Round_Mid', '3.5,4')] // Banker's rounding or standard? Delphi default is Banker's.
        procedure RTL_Round_Works(A: Double; Expected: Int64);

        // --- Comparisons ---
        [Test]
        [TestCase('Eq_True', '=,10,10,1')]
        [TestCase('Neq_True', '<>,10,20,1')]
        procedure RTL_Comparison_Ordinals(const Op: string; A, B: Int64; ExpectedBool: Int64);

        // --- DateTime Logic (New) ---
        [Test]
        procedure RTL_DateTime_ConstructionAndMath;

        // --- Error Handling ---
        [Test]
        procedure RTL_DivByZero_ThrowException;
    end;

implementation

uses
    Myc.Ast;

{ TTestMycAstRTL }

procedure TTestMycAstRTL.Setup;
begin
    FScope := TAst.CreateScope(nil, nil, False);
    Myc.Ast.RTL.RegisterRtlFunctions(FScope);
end;

// --- Helpers ---

function TTestMycAstRTL.ValI(V: Int64): TDataValue;
begin
    Result := TDataValue(TScalar.FromInt64(V));
end;

function TTestMycAstRTL.ValF(V: Double): TDataValue;
begin
    Result := TDataValue(TScalar.FromDouble(V));
end;

function TTestMycAstRTL.Call(const Name: string; const Args: array of TDataValue): TDataValue;
var
    addr: TResolvedAddress;
    funcVal: TDataValue;
    func: TDataValue.TFunc;
    argArray: TArray<TDataValue>;
    i: Integer;
begin
    addr := FScope.Resolve(Name);
    Assert.AreNotEqual(TAddressKind.akUnresolved, addr.Kind, 'Function not found: ' + Name);

    funcVal := FScope[addr];
    func := funcVal.AsMethod();

    SetLength(argArray, Length(Args));
    for i := 0 to High(Args) do
        argArray[i] := Args[i];

    Result := func(argArray);
end;

// --- Tests ---

procedure TTestMycAstRTL.RTL_Registration_SymbolsArePresent;
begin
    Assert.AreNotEqual(TAddressKind.akUnresolved, FScope.Resolve('div').Kind);
    Assert.AreNotEqual(TAddressKind.akUnresolved, FScope.Resolve('mod').Kind);
    Assert.AreNotEqual(TAddressKind.akUnresolved, FScope.Resolve('Date').Kind);
    Assert.AreNotEqual(TAddressKind.akUnresolved, FScope.Resolve('Round').Kind);
end;

procedure TTestMycAstRTL.RTL_Math_Float(const Op: string; A, B, Expected: Double);
var
    res: TDataValue;
begin
    res := Call(Op, [ValF(A), ValF(B)]);
    Assert.AreEqual(TDataValueKind.vkScalar, res.Kind);
    Assert.AreEqual(Expected, res.AsScalar.Value.AsDouble, 0.00001);
end;

procedure TTestMycAstRTL.RTL_Math_Integer(const Op: string; A, B, Expected: Int64);
var
    res: TDataValue;
begin
    res := Call(Op, [ValI(A), ValI(B)]);
    Assert.AreEqual(TDataValueKind.vkScalar, res.Kind);
    Assert.AreEqual(TScalar.TKind.Ordinal, res.AsScalar.Kind);
    Assert.AreEqual(Expected, res.AsScalar.Value.AsInt64);
end;

procedure TTestMycAstRTL.RTL_Bitwise(const Op: string; A, B, Expected: Int64);
var
    res: TDataValue;
begin
    res := Call(Op, [ValI(A), ValI(B)]);
    Assert.AreEqual(TScalar.TKind.Ordinal, res.AsScalar.Kind);
    Assert.AreEqual(Expected, res.AsScalar.Value.AsInt64);
end;

procedure TTestMycAstRTL.RTL_Round_Works(A: Double; Expected: Int64);
var
    res: TDataValue;
begin
    res := Call('Round', [ValF(A)]);
    Assert.AreEqual(TScalar.TKind.Ordinal, res.AsScalar.Kind);
    Assert.AreEqual(Expected, res.AsScalar.Value.AsInt64);
end;

procedure TTestMycAstRTL.RTL_Comparison_Ordinals(const Op: string; A, B: Int64; ExpectedBool: Int64);
var
    res: TDataValue;
begin
    res := Call(Op, [ValI(A), ValI(B)]);
    // Note: TScalar.Equal/NotEqual returns Boolean Kind now!
    // The test case expects integer 0 or 1, so we convert or check bool.
    // TScalar.Implicit(Boolean) -> Int64 (0/1) works.

    Assert.AreEqual(TDataValueKind.vkScalar, res.Kind);
    Assert.AreEqual(TScalar.TKind.Boolean, res.AsScalar.Kind);

    var asInt: Int64 := res.AsScalar; // Implicit conversion
    Assert.AreEqual(ExpectedBool, asInt);
end;

procedure TTestMycAstRTL.RTL_DateTime_ConstructionAndMath;
var
    d, d2, diff: TDataValue;
    expectedDate: TDateTime;
begin
    // 1. Test Date(Y, M, D)
    expectedDate := EncodeDate(2023, 10, 5);
    d := Call('Date', [ValI(2023), ValI(10), ValI(5)]);

    Assert.AreEqual(TScalar.TKind.DateTime, d.AsScalar.Kind);
    Assert.AreEqual(expectedDate, d.AsScalar.Value.AsDouble, 0.001);

    // 2. Test Date + Int (Days)
    d2 := Call('+', [d, ValI(2)]); // Add 2 days
    Assert.AreEqual(TScalar.TKind.DateTime, d2.AsScalar.Kind);
    Assert.AreEqual(expectedDate + 2, d2.AsScalar.Value.AsDouble, 0.001);

    // 3. Test Date - Date (Diff in days)
    diff := Call('-', [d2, d]);
    Assert.AreEqual(TScalar.TKind.Float, diff.AsScalar.Kind);
    Assert.AreEqual(2.0, diff.AsScalar.Value.AsDouble, 0.001);
end;

procedure TTestMycAstRTL.RTL_DivByZero_ThrowException;
begin
    // Integer div
    Assert.WillRaise(procedure begin Call('div', [ValI(10), ValI(0)]); end);
    // Float divide (explicit check in RTL)
    Assert.WillRaise(procedure begin Call('/', [ValF(10.0), ValF(0.0)]); end);
end;

end.
