# ============================================================================
# Key Metadata (keymeta) Test Suite
# ============================================================================
#
# Tests the Redis module key metadata framework: up to 7 independent metadata
# classes (IDs 1-7) can be attached to keys. Class ID 0 is reserved for key
# expiration.
#
# The following features are sensitive to Key Metadata and are tested here:
#
# - KEY EXPIRATION (class ID 0)
#   - Stored at ((uint64_t *)kv) - 1 (first metadata slot)
#   - Managed via db->expires dictionary
#   - Must be preserved/updated when kvobj is reallocated
# - HASH FIELD EXPIRATION (HFE)
#   - NOT in kvobj metadata slots (Maybe in the future...)
#   - Managed via db->hexpires ebuckets (holds direct kvobj pointer)
#   - Must be removed before kvobj reallocation (hashTypeRemoveFromExpires)
#      and restored after (hashTypeAddToExpires)
# - MODULE METADATA (class IDs 1-7)
#   - Defines metadata lifecycle via callbacks
# - EMBEDDED STRINGS vs. REGULAR OBJECTS
#   - Short strings and numbers are embedded into kvobj
#   - The rest are kept as distinct objects
# - LAZYFREE
# ============================================================================

set testmodule [file normalize tests/modules/test_keymeta.so]

# Helper procedure to convert class ID to 4-char-id name
proc cname {cid} {
    return "KMT$cid"
}

# Helper procedure to check if a class should keep metadata for a given operation
proc shouldKeep {cid operation classesSpec} {
    upvar $classesSpec specs
    set spec $specs($cid)
    switch $operation {
        "copy"   { return [string match "*KEEPONCOPY*" $spec] }
        "rename" { return [string match "*KEEPONRENAME*" $spec] }
        "move"   { return [string match "*KEEPONMOVE*" $spec] }
        default  { return 0 }
    }
}

# Helper procedure to setup a key with metadata
proc setupKeyMeta {keyname numClasses expiryBefore expiryAfter} {
    # Set expiry if requested
    if {$expiryBefore} {
        r expire $keyname 10000
        assert_range [r ttl $keyname] 9990 10000
    }

    # Set metadata for all classes
    for {set i 1} {$i <= $numClasses} {incr i} {
        # Set twice to verify overwrite behavior
        r keymeta.set [cname $i] $keyname "blabla$i"
        assert_equal [r keymeta.get [cname $i] $keyname] "blabla$i"
        r keymeta.set [cname $i] $keyname "meta$i"
    }

    # Verify metadata was set correctly
    for {set i 1} {$i <= $numClasses} {incr i} {
        assert_equal [r keymeta.get [cname $i] $keyname] "meta$i"
    }

    if {$expiryAfter} {
        r expire $keyname 10000
        assert_range [r ttl $keyname] 9990 10000
    }

    if {$expiryBefore} {
        assert_range [r ttl $keyname] 9990 10000
    }
}

# Helper procedure to verify metadata after an operation
proc verifyKeyMeta {keyname operation numClasses hasExpiry classesSpec} {
    upvar $classesSpec specs

    # Verify expiry
    if {$hasExpiry} {
        assert_range [r ttl $keyname] 9990 10000
    }

    # Verify metadata based on class spec
    for {set i 1} {$i <= $numClasses} {incr i} {
        set expected [expr {[shouldKeep $i $operation specs] ? "meta$i" : ""}]
        assert_equal [r keymeta.get [cname $i] $keyname] $expected
    }
}

proc flushallAndVerifyCleanup {} {
    r flushall
    # Verify all metadata is cleaned up properly
    assert_equal [r keymeta.active] 0
}

start_server {tags {"modules" "external:skip" "cluster:skip"} overrides {enable-debug-command yes}} {
    r module load $testmodule
    r debug enable-keymeta-runtime-registration 1

    array set classesSpec {}
    set classesSpec(1) "KEEPONCOPY:KEEPONRENAME:KEEPONMOVE:ALLOWIGNORE:RDBLOAD:RDBSAVE"
    set classesSpec(2) "KEEPONCOPY:KEEPONRENAME:UNLINKFREE:ALLOWIGNORE:RDBLOAD:RDBSAVE"
    set classesSpec(3) "KEEPONCOPY:ALLOWIGNORE:RDBLOAD:RDBSAVE"
    set classesSpec(4) "ALLOWIGNORE:RDBLOAD:RDBSAVE"
    set classesSpec(5) "KEEPONRENAME:KEEPONMOVE:ALLOWIGNORE:RDBLOAD:RDBSAVE"
    set classesSpec(6) "KEEPONRENAME:ALLOWIGNORE:RDBLOAD:RDBSAVE"
    set classesSpec(7) "KEEPONMOVE:UNLINKFREE:ALLOWIGNORE:RDBLOAD:RDBSAVE"

    array set classes {}
    for {set cid 1} {$cid <= 7} {incr cid} {
        set spec $classesSpec($cid)
        set classes($cid) [r keymeta.register [cname $cid] 1 $spec]
        puts "Registered class $cid with spec $spec"
        assert_equal $classes($cid) $cid
    }

    # Validates metadata behavior across COPY/RENAME/MOVE operations
    # with varying numbers of metadata classes (1-7), key expiration states,
    # key types (string/hash), hash field expiration, and metadata class flags
    # (KEEPONCOPY, KEEPONRENAME, KEEPONMOVE).
    for {set numClasses 1} {$numClasses < 8} {incr numClasses} {
        foreach expiryBefore {0 1} {
            foreach expiryAfter {0 1} {
                set hasExpiry [expr {$expiryBefore || $expiryAfter}]
                set expiryStr "expiryBefore=$expiryBefore, expiryAfter=$expiryAfter)"
                # Test COPY operation
                test "KEYMETA - copy key-string with $numClasses classes, $expiryStr" {
                    foreach value { 3 "value1" [string repeat "ABCD" 1000]} {
                        r select 0
                        r del k1 k2
                        r set k1 $value
                        setupKeyMeta k1 $numClasses $expiryBefore $expiryAfter
                        # Copy:
                        r copy k1 k2
                        # Verify:
                        assert_equal [r get k1] $value
                        assert_equal [r get k2] $value
                        # Verify expiry and metadata
                        verifyKeyMeta k2 "copy" $numClasses $hasExpiry classesSpec
                        flushallAndVerifyCleanup
                    }
                }

                test "KEYMETA - copy key-hash with $numClasses classes, $expiryStr" {
                    r select 0
                    r del h1 h2
                    r HSET h1 field1 "value1" field2 "value2"
                    r hexpire h1 10000 FIELDS 1 field1
                    setupKeyMeta h1 $numClasses $expiryBefore $expiryAfter
                    # Copy:
                    r copy h1 h2
                    # Verify:
                    verifyKeyMeta h2 "copy" $numClasses $hasExpiry classesSpec
                    assert_range [r httl h1 FIELDS 1 field1] 9999 10000
                    assert_range [r httl h2 FIELDS 1 field1] 9999 10000
                    flushallAndVerifyCleanup
                }

                # Test RENAME operation
                test "KEYMETA - rename key-string with $numClasses classes, $expiryStr" {
                    foreach value { 3 "value1" [string repeat "ABCD" 1000]} {
                        r select 0
                        r del k1 k2
                        r set k1 $value
                        setupKeyMeta k1 $numClasses $expiryBefore $expiryAfter
                        # Rename:
                        r rename k1 k2
                        # Verify:
                        assert_equal [r exists k1] 0
                        assert_equal [r get k2] $value
                        # Verify expiry and metadata
                        verifyKeyMeta k2 "rename" $numClasses $hasExpiry classesSpec
                        flushallAndVerifyCleanup
                    }
                }

                test "KEYMETA - rename key-hash with $numClasses classes, $expiryStr" {
                    r select 0
                    r del h1 h2
                    r HSET h1 field1 "value1" field2 "value2"
                    r hexpire h1 10000 FIELDS 1 field1
                    setupKeyMeta h1 $numClasses $expiryBefore $expiryAfter
                    # Rename:
                    r rename h1 h2
                    # Verify:
                    assert_equal [r exists h1] 0
                    assert_range [r httl h2 FIELDS 1 field1] 9999 10000
                    verifyKeyMeta h2 "rename" $numClasses $hasExpiry classesSpec
                    flushallAndVerifyCleanup
                }



                # Test MOVE operation
                test "KEYMETA - move key-string with $numClasses classes, $expiryStr" {
                    foreach value { 3 "value1" [string repeat "ABCD" 1000]} {
                        r select 9
                        r del k1
                        r select 0
                        r del k1
                        r set k1 $value
                        setupKeyMeta k1 $numClasses $expiryBefore $expiryAfter
                        # Perform move
                        assert_equal [r move k1 9] 1
                        # Verify key moved
                        assert_equal [r exists k1] 0
                        r select 9
                        assert_equal [r get k1] $value
                        # Verify expiry and metadata
                        verifyKeyMeta k1 "move" $numClasses $hasExpiry classesSpec
                        r select 0
                        flushallAndVerifyCleanup
                    }
                }

                test "KEYMETA - move key-hash with $numClasses classes, $expiryStr" {
                    r select 9
                    r del h1
                    r select 0
                    r del h1
                    r HSET h1 field1 "value1" field2 "value2"
                    r hexpire h1 10000 FIELDS 1 field1
                    setupKeyMeta h1 $numClasses $expiryBefore $expiryAfter
                    assert_range [r httl h1 FIELDS 1 field1] 9999 10000
                    assert_equal [r move h1 9] 1
                    assert_equal [r exists h1] 0
                    r select 9
                    assert_range [r httl h1 FIELDS 1 field1] 9999 10000
                    verifyKeyMeta h1 "move" $numClasses $hasExpiry classesSpec
                    r select 0
                    flushallAndVerifyCleanup
                }
            }
        }
    }

    test "KEYMETA - Verify active metadata count on copy" {
        for {set cid 1} {$cid < 7} {incr cid} {
            set numAlloc 0
            flushallAndVerifyCleanup
            set dupOnCopy [shouldKeep $cid "copy" classesSpec]
            r set k1 "v1"
            r keymeta.set [cname $cid] k1 "meta1"
            assert_equal [r keymeta.active] [incr numAlloc]
            r keymeta.set [cname $cid] k1 "meta1b"
            assert_equal [r keymeta.active] $numAlloc
            r copy k1 k1copy
            assert_equal [r keymeta.active] [incr numAlloc $dupOnCopy]
            r del k1
            assert_equal [r keymeta.active] [incr numAlloc -1]
            r del k1copy
            assert_equal [r keymeta.active] 0
        }
    }

    test "KEYMETA - Verify active metadata count on rename" {
        for {set cid 1} {$cid <= 7} {incr cid} {
            set numAlloc 0
            flushallAndVerifyCleanup
            set keepOnRename [shouldKeep $cid "rename" classesSpec]
            set discOnRename [expr {!$keepOnRename}]
            r set k1 "v1"
            r keymeta.set [cname $cid] k1 "meta1"
            assert_equal [r keymeta.active] [incr numAlloc]
            r rename k1 k1_renamed
            assert_equal [r keymeta.active] [incr numAlloc -$discOnRename]
            r del k1_renamed
            assert_equal [r keymeta.active] 0
        }
    }

    test "KEYMETA - Verify active metadata count on move" {
        for {set cid 1} {$cid <= 7} {incr cid} {
            set numAlloc 0
            r select 0
            flushallAndVerifyCleanup

            set keepOnMove [shouldKeep $cid "move" classesSpec]
            set discOnMove [expr {!$keepOnMove}]

            # Create keys with metadata in DB 0
            r set k1 "v1"
            r keymeta.set [cname $cid] k1 "meta1"
            assert_equal [r keymeta.active] [incr numAlloc]
            # Move: metadata discarded if !keepOnMove
            r move k1 9
            set active [r keymeta.active]
            assert_equal [r keymeta.active] [incr numAlloc -$discOnMove]
            # Cleanup
            r select 9
            r del k1
            r select 0
            assert_equal [r keymeta.active] 0
        }
    }

    test "KEYMETA - Verify metadta cleanup on lazyfree" {
        r config set lazyfree-lazy-user-del yes
        # Class 2 has UNLINKFREE flag, so it should call unlink callback when lazyfree is enabled
        # Class 1 does not have UNLINKFREE flag, so it should only call free callback
        foreach {cid} { 1 2 } {
            r config resetstat
            # Create a large unsorted set collection to ensure it exceeds LAZYFREE_THRESHOLD
            for {set i 0} {$i < 1024} {incr i} { r sadd myset $i }
            r keymeta.set [cname $cid] myset "meta"
            assert_equal [r keymeta.active] 1
            r del myset

            # Wait for lazyfree to complete and verify lazyfreed_objects incremented
            wait_for_condition 50 100 {
                [s lazyfree_pending_objects] == 0
            } else {
                fail "lazyfree isn't done"
            }
            assert_equal [r keymeta.active] 0
            assert_equal [s lazyfreed_objects] 1
        }
        r config set lazyfree-lazy-user-del no
    } {OK} {needs:config-resetstat}

    test "KEYMETA - Verify metadata cleanup on expire" {
        # Class 2 has UNLINKFREE flag, so it should call unlink callback when lazyfree is enabled
        # Class 1 does not have UNLINKFREE flag, so it should only call free callback
        foreach {cid} { 1 2 } {
            r set mykey "mykey$cid"
            r keymeta.set [cname $cid] mykey "meta"
            assert_equal [r keymeta.active] 1
            r pexpire mykey 1
            wait_for_condition 50 100 {
                [r exists mykey] == 0
            } else {
                fail "key not expired"
            }
            assert_equal [r keymeta.active] 0
        }
    }

    # ============================================================================
    # AOF Rewrite Tests
    # ============================================================================
    # Note: Full AOF round-trip tests (write → restart → load) are not included
    # because the test module registers classes dynamically via commands, which
    # creates a chicken-and-egg problem:
    # - Classes must be registered BEFORE AOF loading (in RedisModule_OnLoad)
    # - But the KEYMETA.REGISTER commands are in the AOF itself
    # - When server restarts and loads AOF, classes aren't registered yet
    # - KEYMETA.SET commands fail with "metadata class not found"
    #
    # For production modules, classes MUST be registered in RedisModule_OnLoad()
    # to ensure they're available when AOF/RDB files are loaded on server startup.
    # See src/module.c documentation for RM_CreateKeyMetaClass() for details.
    #
    # The test below verifies that AOF callbacks correctly emit KEYMETA.SET commands
    # to the AOF file during rewrite, which is the module's responsibility.
    test "KEYMETA - AOF rewrite emits correct KEYMETA.SET commands to file" {
        # This test verifies that the AOF callback implementation correctly writes
        # KEYMETA.SET commands to the AOF file during rewrite. We don't test the
        # full round-trip (restart + load) due to the dynamic registration limitation
        # explained above.

        r config set appendonly yes
        r config set auto-aof-rewrite-percentage 0
        r config set aof-use-rdb-preamble no
        # Wait for the initial AOF rewrite that Redis triggers when enabling AOF
        waitForBgrewriteaof r

        # Create keys with metadata from multiple classes
        r set key1 "value1"
        r keymeta.set [cname 1] key1 "metadata_c1"

        r set key2 "value2"
        r keymeta.set [cname 2] key2 "metadata_c2"
        r keymeta.set [cname 3] key2 "metadata_c3"

        r hset hashkey field1 val1
        r keymeta.set [cname 4] hashkey "hash_meta"

        # Trigger AOF rewrite
        r bgrewriteaof
        waitForBgrewriteaof r

        # Get the AOF directory and read the AOF file
        set aof_dir [lindex [r config get dir] 1]
        set aof_base_filename [lindex [r config get appendfilename] 1]

        # Find the base AOF file (after rewrite)
        set aof_files [glob -nocomplain -directory $aof_dir appendonlydir/${aof_base_filename}.*.base.aof]
        assert {[llength $aof_files] > 0}

        # Read the most recent base AOF file
        set aof_file [lindex [lsort $aof_files] end]
        set fp [open $aof_file r]
        set aof_content [read $fp]
        close $fp

        # Verify the AOF contains KEYMETA.SET commands with correct format
        assert_match "*KEYMETA.SET*[cname 1]*key1*metadata_c1*" $aof_content
        assert_match "*KEYMETA.SET*[cname 2]*key2*metadata_c2*" $aof_content
        assert_match "*KEYMETA.SET*[cname 3]*key2*metadata_c3*" $aof_content
        assert_match "*KEYMETA.SET*[cname 4]*hashkey*hash_meta*" $aof_content

        # Verify the RESP format is correct by checking for the command structure
        # The AOF should contain: *4 (array of 4 elements)
        assert_match "*\$11*KEYMETA.SET*" $aof_content
        # Count how many KEYMETA.SET commands are in the AOF
        set keymeta_count [regexp -all {KEYMETA\.SET} $aof_content]
        assert_equal $keymeta_count 4
    } {} {external:skip}

    # ========================================================================
    # RDB Save/Load Tests
    # ========================================================================

    test {RDB: SAVE and reload preserves metadata} {
        # Create key with metadata
        r set key1 "value1"
        r keymeta.set [cname 1] key1 "key1_meta1"
        assert_equal [r keymeta.get [cname 1] key1] "key1_meta1"

        r save
        r debug reload

        # Verify metadata persisted after reload
        assert_equal [r keymeta.get [cname 1] key1] "key1_meta1"

        flushallAndVerifyCleanup
    } {} {external:skip needs:save}

    test {RDB: BGSAVE writes metadata to RDB file} {
        # Create keys with different metadata combinations
        r set key1 "value1"
        r keymeta.set [cname 1] key1 "key1_meta1"

        r set key2 "value2"
        r keymeta.set [cname 1] key2 "key2_meta1"
        r keymeta.set [cname 2] key2 "key2_meta2"

        # Trigger BGSAVE and reload (debug reload preserves modules)
        r bgsave
        waitForBgsave r
        r debug reload

        # Verify metadata persisted after reload
        assert_equal [r keymeta.get [cname 1] key1] "key1_meta1"
        assert_equal [r keymeta.get [cname 1] key2] "key2_meta1"
        assert_equal [r keymeta.get [cname 2] key2] "key2_meta2"

        flushallAndVerifyCleanup
    } {} {external:skip needs:save}

    test {RDB: Metadata persists with expiretime} {
        # Create key with both expiry and metadata
        r set key1 "value1"
        set expire_time [expr {[clock seconds] + 10000}]
        r expireat key1 $expire_time
        r keymeta.set [cname 1] key1 "meta_with_expire"

        assert_equal [r expiretime key1] $expire_time
        assert_equal [r keymeta.get [cname 1] key1] "meta_with_expire"

        # Reload from RDB
        r debug reload

        # Verify metadata and expiry persist after reload
        assert_equal [r expiretime key1] $expire_time
        assert_equal [r keymeta.get [cname 1] key1] "meta_with_expire"

        flushallAndVerifyCleanup
    } {} {external:skip needs:debug}

    test {RDB: Create keys with upto 7 meta classes, with or without expiry} {
        # Test all combinations of 1-7 metadata classes, with or without expiry
        for {set n 1} {$n <= 7} {incr n} {
            foreach hasExpiry {0 1} {
                set keyname "key_${n}_exp${hasExpiry}"
                r set $keyname "value$n"

                # Set expiry if hasExpiry is 1
                if {$hasExpiry} {
                    set ttl [expr {3600 + $n}]
                    r expire $keyname $ttl
                    # Get the actual expiretime set by Redis to use as expected value
                    set expExpiry [r expiretime $keyname]
                }

                # Create list of class IDs to attach (1 through n)
                set class_ids {}
                for {set i 1} {$i <= $n} {incr i} {
                    lappend class_ids $i
                }

                # Randomize the order of metadata attachment
                set class_ids [lshuffle $class_ids]

                # Attach metadata in randomized order
                foreach cid $class_ids {
                    r keymeta.set [cname $cid] $keyname "meta$cid"
                }

                # Verify metadata before RDB save
                # Verify exactly n metadata classes are attached
                for {set i 1} {$i <= 7} {incr i} {
                    if {$i <= $n} {
                        assert_equal [r keymeta.get [cname $i] $keyname] "meta$i"
                    } else {
                        assert_equal [r keymeta.get [cname $i] $keyname] ""
                    }
                }

                # Verify expiry before RDB save
                if {$hasExpiry} {
                    set actual_expiretime [r expiretime $keyname]
                    assert_equal $actual_expiretime $expExpiry
                }

                # Save and reload from RDB (debug reload preserves modules)
                r save
                r debug reload

                # Verify metadata after RDB reload
                # Verify exactly n metadata classes are still attached
                for {set i 1} {$i <= 7} {incr i} {
                    if {$i <= $n} {
                        assert_equal [r keymeta.get [cname $i] $keyname] "meta$i"
                    } else {
                        assert_equal [r keymeta.get [cname $i] $keyname] ""
                    }
                }

                # Verify expiry after RDB reload
                if {$hasExpiry} {
                    set actual_expiretime [r expiretime $keyname]
                    assert_equal $actual_expiretime $expExpiry
                } else {
                    # Verify no expiry set
                    assert_equal [r expiretime $keyname] -1
                }
                flushallAndVerifyCleanup
            }
        }
    } {} {external:skip needs:save}

    # ========================================================================
    # RDB Flag Tests: ALLOW_IGNORE, RDBLOAD, RDBSAVE
    # ========================================================================

    # Test all combinations except the error case (ALLOW_IGNORE=0, RDBLOAD=0, RDBSAVE=1)
    foreach RDBLOAD {0 1} {
        foreach RDBSAVE {0 1} {
            foreach ALLOW_IGNORE {0 1} {
                # Skip the error case - we'll test it last since it causes RDB load to fail
                if {!$RDBLOAD && $RDBSAVE && !$ALLOW_IGNORE} { continue }

                test "RDB: SAVE and LOAD (ALLOW_IGNORE=$ALLOW_IGNORE, RDBLOAD=$RDBLOAD, RDBSAVE=$RDBSAVE)" {
                    # Flush all data and save empty RDB to start with a clean slate
                    r flushall
                    r save

                    # re-register class 1 with new flags. Expected re-registered same class ID
                    r keymeta.unregister [cname 1]
                    # dummy default spec
                    set newSpec "KEEPONCOPY"
                    if {$ALLOW_IGNORE} { append newSpec ":ALLOWIGNORE" }
                    if {$RDBLOAD} { append newSpec ":RDBLOAD" }
                    if {$RDBSAVE} { append newSpec ":RDBSAVE" }

                    # Must reuse same class-id that it had before
                    assert_equal $classes(1) [r keymeta.register [cname 1] 1 $newSpec]

                    r set key1 "value1"
                    r keymeta.set [cname 1] key1 "key1_meta1"
                    assert_equal [r keymeta.get [cname 1] key1] "key1_meta1"

                    r save
                    r debug reload

                    # Metadata is preserved only when BOTH rdb_save AND rdb_load are enabled
                    # Otherwise metadata is lost (either not saved, or saved but not loaded)
                    set metaPreserved [expr {$RDBSAVE && $RDBLOAD}]
                    set expectedMeta [expr {$metaPreserved ? "key1_meta1" : ""}]

                    assert_equal [r keymeta.get [cname 1] key1] $expectedMeta

                    flushallAndVerifyCleanup
                } {} {external:skip needs:save}
            }
        }
    }

    # Test the error case last (ALLOW_IGNORE=0, RDBLOAD=0, RDBSAVE=1)
    # This test causes RDB load to fail, so we test it last to avoid polluting subsequent tests
    test "RDB: SAVE and LOAD Invalid combination: (ALLOW_IGNORE=0, RDBLOAD=0, RDBSAVE=1)" {
        # re-register class 1 with RDBSAVE flag but no RDBLOAD or ALLOW_IGNORE
        r keymeta.unregister [cname 1]
        set newSpec "KEEPONCOPY:RDBSAVE"
        assert_equal $classes(1) [r keymeta.register [cname 1] 1 $newSpec]

        r set key1 "value1"
        r keymeta.set [cname 1] key1 "key1_meta1"
        assert_equal [r keymeta.get [cname 1] key1] "key1_meta1"

        r save

        # This combination causes RDB load to fail because:
        # - Metadata was saved (RDBSAVE=1)
        # - Class has no rdb_load callback (RDBLOAD=0)
        # - Errors are not ignored (ALLOW_IGNORE=0)
        catch {r debug reload} err
        assert_match "*Error trying to load the RDB dump*" $err
    } {} {external:skip needs:save}

    # ========================================================================
    # DUMP/RESTORE Tests
    # ========================================================================

    test {DUMP/RESTORE: 1 to 7 metadata classes, optional TTL} {
        foreach withTTL {0 1} {
            for {set numClasses 1} {$numClasses < 8} {incr numClasses} {
                # Re-register classes with RDBLOAD and RDBSAVE flags
                for {set cid 1} {$cid <= $numClasses} {incr cid} {
                    r keymeta.unregister [cname $cid]
                    assert_equal $classes($cid) [r keymeta.register [cname $cid] 1 $classesSpec($cid)]
                }

                # Create key with metadata classes
                r set key1 "value1"
                for {set i 1} {$i <= $numClasses} {incr i} {
                    r keymeta.set [cname $i] key1 "meta${i}_value"
                }

                if {$withTTL} { r expire key1 10000 }

                # Verify all metadata before DUMP
                for {set i 1} {$i <= $numClasses} {incr i} {
                    assert_equal [r keymeta.get [cname $i] key1] "meta${i}_value"
                }

                # DUMP the key
                set encoded [r dump key1]

                # Delete and RESTORE
                r del key1
                r restore key1 [expr {$withTTL ? 10000 : 0}] $encoded

                # Verify all metadata was restored
                assert_equal [r get key1] "value1"
                for {set i 1} {$i <= $numClasses} {incr i} {
                    assert_equal [r keymeta.get [cname $i] key1] "meta${i}_value"
                }
                if {$withTTL} { assert_range [r pttl key1] 9000 10000 }

                flushallAndVerifyCleanup
            }
        }
    }

    test {DUMP/RESTORE: REPLACE with metadata} {
        # Create key with metadata
        r set key1 value1
        r keymeta.set [cname 1] key1 "meta1_original"

        # DUMP the key
        set encoded1 [r dump key1]

        # Create different key with different metadata
        r set key1 value2
        r keymeta.set [cname 1] key1 "meta1_new"

        # DUMP the second version
        set encoded2 [r dump key1]

        # Delete and restore first version
        r del key1
        r restore key1 0 $encoded1
        assert_equal [r get key1] "value1"
        assert_equal [r keymeta.get [cname 1] key1] "meta1_original"

        # RESTORE second version with REPLACE
        r restore key1 0 $encoded2 replace
        assert_equal [r get key1] "value2"
        assert_equal [r keymeta.get [cname 1] key1] "meta1_new"

        flushallAndVerifyCleanup
    }


    # Test all combinations except the error case (ALLOW_IGNORE=0, RDBLOAD=0, RDBSAVE=1)
    foreach RDBLOAD {0 1} {
        foreach RDBSAVE {0 1} {
            foreach ALLOW_IGNORE {0 1} {
                # Skip the error case - we'll test it last since it causes RESTORE to fail
                if {!$RDBLOAD && $RDBSAVE && !$ALLOW_IGNORE} { continue }

                test "DUMP/RESTORE: (ALLOW_IGNORE=$ALLOW_IGNORE, RDBLOAD=$RDBLOAD, RDBSAVE=$RDBSAVE)" {
                    # re-register class 1 with new flags. Expected re-registered same class ID
                    r keymeta.unregister [cname 1]
                    # dummy default spec
                    set newSpec "KEEPONCOPY"
                    if {$ALLOW_IGNORE} { append newSpec ":ALLOWIGNORE" }
                    if {$RDBLOAD} { append newSpec ":RDBLOAD" }
                    if {$RDBSAVE} { append newSpec ":RDBSAVE" }

                    # Must reuse same class-id that it had before
                    assert_equal $classes(1) [r keymeta.register [cname 1] 1 $newSpec]

                    r set key1 "value1"
                    r keymeta.set [cname 1] key1 "key1_meta1"
                    assert_equal [r keymeta.get [cname 1] key1] "key1_meta1"

                    # DUMP & RESTORE
                    set encoded [r dump key1]
                    r del key1
                    r restore key1 0 $encoded

                    # Metadata is preserved only when BOTH rdb_save AND rdb_load are enabled
                    # Otherwise metadata is lost (either not saved, or saved but not loaded)
                    set metaPreserved [expr {$RDBSAVE && $RDBLOAD}]
                    set expectedMeta [expr {$metaPreserved ? "key1_meta1" : ""}]

                    assert_equal [r keymeta.get [cname 1] key1] $expectedMeta

                    flushallAndVerifyCleanup
                }
            }
        }
    }

    # Test the error case last (ALLOW_IGNORE=0, RDBLOAD=0, RDBSAVE=1)
    # This test causes RESTORE to fail, so we test it last to avoid polluting subsequent tests
    test "DUMP/RESTORE: Invalid combination: (ALLOW_IGNORE=0, RDBLOAD=0, RDBSAVE=1)" {
        # re-register class 1 with RDBSAVE flag but no RDBLOAD or ALLOW_IGNORE
        r keymeta.unregister [cname 1]
        set newSpec "KEEPONCOPY:RDBSAVE"
        assert_equal $classes(1) [r keymeta.register [cname 1] 1 $newSpec]

        r set key1 "value1"
        r keymeta.set [cname 1] key1 "key1_meta1"
        assert_equal [r keymeta.get [cname 1] key1] "key1_meta1"

        # DUMP the key
        set encoded [r dump key1]

        # Delete and try to RESTORE
        r del key1

        # This combination causes RESTORE to fail because:
        # - Metadata was saved (RDBSAVE=1)
        # - Class has no rdb_load callback (RDBLOAD=0)
        # - Errors are not ignored (ALLOW_IGNORE=0)
        catch {r restore key1 0 $encoded} err
        assert_match "*Bad data format*" $err

        flushallAndVerifyCleanup
    }
}

test "RDB: Load with different module registration order preserves metadata correctly" {
    # This test verifies out-of-order metadata attachment during RDB load.
    # When modules register in different order at load time vs save time,
    # metadata values should still be correctly associated with their classes.
    start_server {tags {"modules" "external:skip" "cluster:skip"} overrides {enable-debug-command yes}} {
        r module load $testmodule
        r debug enable-keymeta-runtime-registration 1

        # Helper function to generate class names (needed in inner scope)
        proc cname {id} { return "CLS$id" }

        # Register classes in order: 1, 2, 3
        set spec1 "KEEPONCOPY:ALLOWIGNORE:RDBLOAD:RDBSAVE"
        set spec2 "KEEPONRENAME:ALLOWIGNORE:RDBLOAD:RDBSAVE"
        set spec3 "KEEPONMOVE:ALLOWIGNORE:RDBLOAD:RDBSAVE"

        set class1 [r keymeta.register [cname 1] 1 $spec1]
        set class2 [r keymeta.register [cname 2] 1 $spec2]
        set class3 [r keymeta.register [cname 3] 1 $spec3]

        # Verify class IDs match registration order
        assert_equal $class1 1 "Class 1 registered first, gets ID 1"
        assert_equal $class2 2 "Class 2 registered second, gets ID 2"
        assert_equal $class3 3 "Class 3 registered third, gets ID 3"

        # OUTER SERVER: Create RDB with classes registered in order 1,2,3
        r flushall
        r set mykey "myvalue"
        r keymeta.set [cname 1] mykey "metadata_for_class1"
        r keymeta.set [cname 2] mykey "metadata_for_class2"
        r keymeta.set [cname 3] mykey "metadata_for_class3"

        # Verify metadata before save
        assert_equal [r keymeta.get [cname 1] mykey] "metadata_for_class1"
        assert_equal [r keymeta.get [cname 2] mykey] "metadata_for_class2"
        assert_equal [r keymeta.get [cname 3] mykey] "metadata_for_class3"

        r save

        # Get RDB file path & Copy RDB to a temp location with unique name
        set rdb_dir [lindex [r config get dir] 1]
        set rdb_file [lindex [r config get dbfilename] 1]
        set rdb_path [file join $rdb_dir $rdb_file]
        set temp_rdb [file join $rdb_dir "temp_metadata_outoforder_[pid].rdb"]
        file copy -force $rdb_path $temp_rdb

        # INNER SERVER: Start new server, register classes in DIFFERENT order, then load RDB
        start_server [list overrides [list dir $rdb_dir enable-debug-command yes]] {
            r module load $testmodule
            r debug enable-keymeta-runtime-registration 1

            # Helper function to generate class names (needed in inner scope)
            proc cname {id} { return "CLS$id" }

            # Register classes in DIFFERENT order: 3, 1, 2
            # This simulates a server where modules load in different order
            set spec1 "KEEPONCOPY:ALLOWIGNORE:RDBLOAD:RDBSAVE"
            set spec2 "KEEPONRENAME:ALLOWIGNORE:RDBLOAD:RDBSAVE"
            set spec3 "KEEPONMOVE:ALLOWIGNORE:RDBLOAD:RDBSAVE"

            set class3 [r keymeta.register [cname 3] 1 $spec3]
            set class1 [r keymeta.register [cname 1] 1 $spec1]
            set class2 [r keymeta.register [cname 2] 1 $spec2]

            # Verify class IDs are assigned by REGISTRATION ORDER, not name
            # We registered in order 3,1,2, so the runtime IDs are:
            # - class3 (name "CLS3") gets ID 1 (first registered)
            # - class1 (name "CLS1") gets ID 2 (second registered)
            # - class2 (name "CLS2") gets ID 3 (third registered)
            # This is DIFFERENT from outer server which registered in order 1,2,3
            assert_equal $class3 1 "Class 3 registered first, gets ID 1"
            assert_equal $class1 2 "Class 1 registered second, gets ID 2"
            assert_equal $class2 3 "Class 2 registered third, gets ID 3"

            # Copy the saved RDB to this server's dbfilename
            set inner_rdb_file [lindex [r config get dbfilename] 1]
            set inner_rdb_path [file join $rdb_dir $inner_rdb_file]
            file copy -force $temp_rdb $inner_rdb_path

            # NOW load the RDB (AFTER registration in different order)
            # Use 'nosave' to reload from the copied RDB without saving current state first
            r debug reload nosave

            # Verify the key exists
            assert_equal [r exists mykey] 1 "Key should exist after RDB load"
            assert_equal [r get mykey] "myvalue" "Key value should be preserved"

            # Verify metadata values are correctly associated with their classes
            # WITHOUT metadata would be swapped because:
            # - At SAVE time (outer): classes registered in order 1,2,3
            # - At LOAD time (inner): classes registered in order 3,1,2
            # - RDB contains metadata in saved order, but keyMetaClassLookupByName
            #   maps them back to correct classes by NAME, not by registration order
            assert_equal [r keymeta.get [cname 1] mykey] "metadata_for_class1"
            assert_equal [r keymeta.get [cname 2] mykey] "metadata_for_class2"
            assert_equal [r keymeta.get [cname 3] mykey] "metadata_for_class3"
        }

        # Cleanup temp file
        file delete $temp_rdb

    }
} {} {external:skip needs:save}

test "RDB: File size same with/without metadata when no rdb_save callback" {
    # This test verifies that when a metadata class has no rdb_save callback,
    # the metadata is not serialized to RDB, so the RDB file size should be
    # approximately the same (within a small tolerance for header differences).

    start_server {tags {"modules" "external:skip" "cluster:skip"} overrides {enable-debug-command yes}} {
        r module load $testmodule
        r debug enable-keymeta-runtime-registration 1

        # Get RDB directory
        set rdb_dir [lindex [r config get dir] 1]
        set rdb_file [lindex [r config get dbfilename] 1]
        set rdb_path [file join $rdb_dir $rdb_file]

        # Test 1: Create key WITHOUT metadata and save
        r flushall
        r set key1 "test_value_12345"
        r save
        set size_without_meta [file size $rdb_path]
        
        # Test 2: Create identical key WITH metadata (but no rdb_save) and save        
        # Register a class WITHOUT rdb_save callback (RDBSAVE=0)
        # Use ALLOWIGNORE so loading doesn't fail when metadata is missing
        set spec "ALLOWIGNORE"
        r keymeta.register [cname 1] 1 $spec
        
        r flushall
        r set key1 "test_value_12345"
        r keymeta.set [cname 1] key1 "some_metadata_value"

        # Verify metadata is attached
        assert_equal [r keymeta.get [cname 1] key1] "some_metadata_value"

        r save
        set size_with_meta [file size $rdb_path]

        # The file sizes should be the same (metadata not serialized)
        assert_equal $size_without_meta $size_with_meta
    }
} {} {external:skip needs:save}

test "Creating key metadata not during OnLoad should fail" {
    # Start server without enabling keymeta runtime registration debug flag
    start_server {tags {"modules" "external:skip" "cluster:skip"} overrides {enable-debug-command no}} {
        r module load $testmodule
        # Creating a class not during server startup should fail
        catch {r keymeta.register [cname 1] 1 "ALLOWIGNORE"} err
        assert_match {*failed to create metadata class*} $err
    }
} {} {external:skip needs:save}
