unit TestDataRecord;

interface

uses
    DunitX.TestFramework,
    System.SysUtils,
    System.DateUtils,
    System.Rtti,
    Myc.Data.Records;

type
    // A record that contains all supported data types for testing
    TMyTestRecord = record
        MyInteger: Integer;
        MyInt64: Int64;
        MySingle: Single;
        MyDouble: Double;
        MyString: string;
        MyTimestamp: TDateTime;
        MyRecord: TDataRecord;
    end;

    // A simple record to be nested inside another
    TNestedTestRecord = record
        NestedValue: Integer;
        NestedString: string;
    end;

    // A record that contains another record field of type TDataRecord
    TOuterTestRecord = record
        OuterValue: Integer;
        MyNestedRecord: TDataRecord;
    end;

    [TestFixture]
    TTestDataRecord = class(TObject)
    public
        [Test]
        [IgnoreMemoryLeaks]
        procedure TestLayoutCreation;

        [Test]
        [IgnoreMemoryLeaks]
        procedure TestCreateFromInstanceAndGetValue;

        [Test]
        [IgnoreMemoryLeaks]
        procedure TestSetValueAndGetValue;

        [Test]
        //        [IgnoreMemoryLeaks]
        procedure TestNestedDataRecord;

        [Test]
        //        [IgnoreMemoryLeaks]
        procedure TestInternalRecordAssignment;

        [Test]
        //        [IgnoreMemoryLeaks]
        procedure TestNestedSetValueAndGetValue;
    end;

implementation

{ TTestDataRecord }

procedure TTestDataRecord.TestLayoutCreation;
var
    layout: TDataRecord.TLayout;
    idx: Integer;
begin
    // Test: Create layout from a record type
    layout := TDataRecord.TLayout.FromRecord<TMyTestRecord>;

    // Assert: Check field count
    Assert.IsTrue(Length(layout.Fields) = 7, 'Layout should have 7 fields');

    // Assert: Check specific fields for correct type mapping
    idx := layout.IndexOf('MyTimestamp');
    Assert.IsTrue(idx > -1, 'Field MyTimestamp not found');
    Assert.AreEqual(TDataRecord.TFieldType.dfTimestamp, layout.Fields[idx].FieldType, 'MyTimestamp should be mapped to dfTimestamp');

    idx := layout.IndexOf('MyInteger');
    Assert.IsTrue(idx > -1, 'Field MyInteger not found');
    Assert.AreEqual(TDataRecord.TFieldType.dfInteger, layout.Fields[idx].FieldType, 'MyInteger should be mapped to dfInteger');

    idx := layout.IndexOf('MyString');
    Assert.IsTrue(idx > -1, 'Field MyString not found');
    Assert.AreEqual(TDataRecord.TFieldType.dfString, layout.Fields[idx].FieldType, 'MyString should be mapped to dfString');

    idx := layout.IndexOf('MySingle');
    Assert.IsTrue(idx > -1, 'Field MySingle not found');
    Assert.AreEqual(TDataRecord.TFieldType.dfFloat, layout.Fields[idx].FieldType, 'MySingle should be mapped to dfFloat');

    idx := layout.IndexOf('MyRecord');
    Assert.IsTrue(idx > -1, 'Field MyRecord not found');
    Assert.AreEqual(TDataRecord.TFieldType.dfRecord, layout.Fields[idx].FieldType, 'MyRecord should be mapped to dfRecord');
end;

procedure TTestDataRecord.TestCreateFromInstanceAndGetValue;
var
    srcRecord: TMyTestRecord;
    dataRecord: TDataRecord;
begin
    // Setup: Create and populate a source record
    srcRecord.MyInteger := 12345;
    srcRecord.MyInt64 := 9876543210;
    srcRecord.MySingle := 123.456;
    srcRecord.MyDouble := 789.012345;
    srcRecord.MyString := 'Hello Delphi!';
    srcRecord.MyTimestamp := EncodeDateTime(2024, 7, 19, 10, 30, 0, 0);
    srcRecord.MyRecord := default(TDataRecord); // Not testing nested here

    // Test: Create a TDataRecord from the source record instance
    dataRecord := TDataRecord.FromRecord<TMyTestRecord>(srcRecord);

    // Assert: Retrieve each value and check for correctness
    Assert.AreEqual(srcRecord.MyInt64, dataRecord.GetValue<Int64>('MyInt64'), 'Int64 value mismatch');
    Assert.AreEqual(srcRecord.MyInteger, dataRecord.GetValue<Integer>('MyInteger'), 'Integer value mismatch');
    Assert.AreEqual(srcRecord.MyString, dataRecord.GetValue<string>('MyString'), 'String value mismatch');
    Assert.AreEqual(srcRecord.MyTimestamp, dataRecord.GetValue<TDateTime>('MyTimestamp'), 'TDateTime value mismatch');

    // Assert: Check float values with a tolerance for precision differences
    Assert.AreEqual(srcRecord.MySingle, dataRecord.GetValue<Single>('MySingle'), 1e-6, 'Single value mismatch');
    Assert.AreEqual(srcRecord.MyDouble, dataRecord.GetValue<Double>('MyDouble'), 1e-9, 'Double value mismatch');

    // Assert: Test type promotion (reading an Integer as Int64)
    Assert.IsTrue(srcRecord.MyInteger = dataRecord.GetValue<Int64>('MyInteger'), 'Integer to Int64 promotion failed');
end;

procedure TTestDataRecord.TestSetValueAndGetValue;
var
    dataRecord: TDataRecord;
    testInt: Integer;
    testInt64: Int64;
    testSingle: Single;
    testDouble: Double;
    testString: string;
    testTimestamp: TDateTime;
begin
    // Setup: Create an empty TDataRecord with the layout of TMyTestRecord
    dataRecord := TDataRecord.FromRecord<TMyTestRecord>;

    // Setup: Define test values
    testInt := 54321;
    testInt64 := 12345678901;
    testSingle := 987.654;
    testDouble := 543.210987;
    testString := 'Testing SetValue';
    testTimestamp := Now;

    // Test: Set values for each field by name
    dataRecord.SetValue<Integer>('MyInteger', testInt);
    dataRecord.SetValue<Int64>('MyInt64', testInt64);
    dataRecord.SetValue<Single>('MySingle', testSingle);
    dataRecord.SetValue<Double>('MyDouble', testDouble);
    dataRecord.SetValue<string>('MyString', testString);
    dataRecord.SetValue<TDateTime>('MyTimestamp', testTimestamp);

    // Assert: Retrieve the values and check if they match what was set
    Assert.AreEqual(testInt, dataRecord.GetValue<Integer>('MyInteger'), 'SetValue failed for Integer');
    Assert.AreEqual(testInt64, dataRecord.GetValue<Int64>('MyInt64'), 'SetValue failed for Int64');
    Assert.AreEqual(testString, dataRecord.GetValue<string>('MyString'), 'SetValue failed for String');
    Assert.AreEqual(testTimestamp, dataRecord.GetValue<TDateTime>('MyTimestamp'), 'SetValue failed for TDateTime');

    Assert.AreEqual(testSingle, dataRecord.GetValue<Single>('MySingle'), 1e-6, 'SetValue failed for Single');
    Assert.AreEqual(testDouble, dataRecord.GetValue<Double>('MyDouble'), 1e-9, 'SetValue failed for Double');
end;

procedure TTestDataRecord.TestNestedDataRecord;
var
    nestedSrc: TNestedTestRecord;
    nestedDataRecord: TDataRecord;
    outerSrc: TOuterTestRecord;
    mainDataRecord: TDataRecord;
    retrievedNestedRecord: TDataRecord;
begin
    // Setup: Create the nested record and convert it to a TDataRecord
    nestedSrc.NestedValue := 999;
    const str = 'Nested Record Test';
    nestedSrc.NestedString := str;
    nestedDataRecord := TDataRecord.FromRecord<TNestedTestRecord>(nestedSrc);

    // Setup: Create the outer record containing the nested TDataRecord
    outerSrc.OuterValue := 111;
    outerSrc.MyNestedRecord := nestedDataRecord;

    // Test: Create the main TDataRecord from the outer record instance
    mainDataRecord := TDataRecord.FromRecord<TOuterTestRecord>(outerSrc);

    // Assert: Retrieve the nested record and check its contents
    retrievedNestedRecord := mainDataRecord.GetValue<TDataRecord>('MyNestedRecord');

    Assert.IsNotNull(retrievedNestedRecord.Layout.Fields, 'Nested record layout should not be nil');
    Assert.IsTrue(Length(retrievedNestedRecord.Layout.Fields) = 2, 'Nested record should have 2 fields');

    // Assert values within the nested record
    Assert.AreEqual(999, retrievedNestedRecord.GetValue<Integer>('NestedValue'), 'Nested integer value mismatch');
    Assert.AreEqual(str, retrievedNestedRecord.GetValue<string>('NestedString'), 'Nested string value mismatch');

    // Assert value in the outer record
    Assert.AreEqual(111, mainDataRecord.GetValue<Integer>('OuterValue'), 'Outer integer value mismatch');
end;

procedure TTestDataRecord.TestInternalRecordAssignment;
var
    nestedSrc: TNestedTestRecord;
    nestedDataRecord: TDataRecord;
    outerSrc: TOuterTestRecord;
    mainDataRecord: TDataRecord;
    retrievedNestedRecord: TDataRecord;
begin
    // Setup: Create the nested record and convert it to a TDataRecord
    const str = 'Nested Record Test';
    nestedSrc.NestedString := str;
    nestedDataRecord := TDataRecord.FromRecord<TNestedTestRecord>(nestedSrc);

    // Setup: Create the outer record containing the nested TDataRecord
    outerSrc.MyNestedRecord := nestedDataRecord;

    // Place a record on the heap. FAILS: This erases the nested record data!! -- Fixed 25.07.2025!
    TDataRecord.FromRecord<TOuterTestRecord>(outerSrc);

    // Test: Create the main TDataRecord from the outer record instance
    mainDataRecord := TDataRecord.FromRecord<TOuterTestRecord>(outerSrc);

    // Assert: Retrieve the nested record and check its contents
    retrievedNestedRecord := mainDataRecord.GetValue<TDataRecord>('MyNestedRecord');

    // Assert values within the nested record
    Assert.AreEqual(str, retrievedNestedRecord.GetValue<string>('NestedString'), 'Nested string value mismatch');
end;

procedure TTestDataRecord.TestNestedSetValueAndGetValue;
var
    outerRecord: TDataRecord;
    nestedRecord: TDataRecord;
    retrievedNestedRecord: TDataRecord;
begin
    // Setup: Create an empty TDataRecord for the outer structure
    outerRecord := TDataRecord.FromRecord<TOuterTestRecord>;

    // Setup: Create a TDataRecord for the nested structure and populate it
    nestedRecord := TDataRecord.FromRecord<TNestedTestRecord>;
    nestedRecord.SetValue<Integer>('NestedValue', 888);
    nestedRecord.SetValue<string>('NestedString', 'Manual Nested Set');

    // Test: Set the nested record and another value in the outer record
    outerRecord.SetValue<Integer>('OuterValue', 222);
    outerRecord.SetValue<TDataRecord>('MyNestedRecord', nestedRecord);

    // Assert: Retrieve the nested record and check its contents
    retrievedNestedRecord := outerRecord.GetValue<TDataRecord>('MyNestedRecord');
    Assert.IsNotNull(retrievedNestedRecord.Layout.Fields, 'Retrieved nested record layout should not be nil');
    Assert.IsTrue(Length(retrievedNestedRecord.Layout.Fields) = 2, 'Retrieved nested record should have 2 fields');

    Assert.AreEqual(888, retrievedNestedRecord.GetValue<Integer>('NestedValue'), 'Nested integer check failed after SetValue');
    Assert
        .AreEqual('Manual Nested Set', retrievedNestedRecord.GetValue<string>('NestedString'), 'Nested string check failed after SetValue');

    // Assert: Check the outer value as well
    Assert.AreEqual(222, outerRecord.GetValue<Integer>('OuterValue'), 'Outer value check failed after SetValue');
end;

initialization
    TDUnitX.RegisterTestFixture(TTestDataRecord);

end.
