#!/usr/bin/env bash
set -e

DTOB="$(dirname "$0")/../bin/dtob"
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT

PASS=0
FAIL=0

strip_ansi() {
    sed $'s/\033\\[[0-9;]*m//g' | tr -s ' '
}

# encode with infer-types (default), check represent output contains pattern
run_test() {
    local name="$1" json="$2" pattern="$3"
    local infile="$TMPDIR/in.json"
    local outfile="$TMPDIR/out.dtob"

    printf '%s' "$json" > "$infile"
    "$DTOB" encode "$infile" "$outfile" 2>/dev/null

    local got
    got=$("$DTOB" represent "$outfile" 2>/dev/null | strip_ansi)

    if echo "$got" | grep -qF "$pattern"; then
        echo "  PASS: $name"
        PASS=$((PASS + 1))
    else
        echo "  FAIL: $name"
        echo "    expected to contain: $pattern"
        echo "    got:"
        echo "$got" | head -20 | sed 's/^/      /'
        FAIL=$((FAIL + 1))
    fi
}

# check that represent output does NOT contain a pattern
run_test_absent() {
    local name="$1" json="$2" pattern="$3"
    local infile="$TMPDIR/in.json"
    local outfile="$TMPDIR/out.dtob"

    printf '%s' "$json" > "$infile"
    "$DTOB" encode "$infile" "$outfile" 2>/dev/null

    local got
    got=$("$DTOB" represent "$outfile" 2>/dev/null | strip_ansi)

    if echo "$got" | grep -qF "$pattern"; then
        echo "  FAIL: $name"
        echo "    should NOT contain: $pattern"
        echo "    got:"
        echo "$got" | head -20 | sed 's/^/      /'
        FAIL=$((FAIL + 1))
    else
        echo "  PASS: $name"
        PASS=$((PASS + 1))
    fi
}

# check file size matches expected
run_test_size() {
    local name="$1" json="$2" expected_size="$3"
    local infile="$TMPDIR/in.json"
    local outfile="$TMPDIR/out.dtob"

    printf '%s' "$json" > "$infile"
    "$DTOB" encode "$infile" "$outfile" 2>/dev/null

    local got_size
    got_size=$(wc -c < "$outfile" | tr -d ' ')

    if [ "$got_size" = "$expected_size" ]; then
        echo "  PASS: $name"
        PASS=$((PASS + 1))
    else
        echo "  FAIL: $name"
        echo "    expected size: $expected_size"
        echo "    got size:      $got_size"
        FAIL=$((FAIL + 1))
    fi
}

# roundtrip: encode then re-encode with --infer-types=0, sizes should differ
run_test_smaller() {
    local name="$1" json="$2"
    local infile="$TMPDIR/in.json"
    local typed="$TMPDIR/typed.dtob"
    local plain="$TMPDIR/plain.dtob"

    printf '%s' "$json" > "$infile"
    "$DTOB" encode "$infile" "$typed" 2>/dev/null
    "$DTOB" encode "$infile" "$plain" --infer-types=0 2>/dev/null

    local sz_typed sz_plain
    sz_typed=$(wc -c < "$typed" | tr -d ' ')
    sz_plain=$(wc -c < "$plain" | tr -d ' ')

    if [ "$sz_typed" -lt "$sz_plain" ]; then
        echo "  PASS: $name (typed=$sz_typed < plain=$sz_plain)"
        PASS=$((PASS + 1))
    else
        echo "  FAIL: $name (typed=$sz_typed >= plain=$sz_plain)"
        FAIL=$((FAIL + 1))
    fi
}

echo "=== infer-types nested/recursive tests ==="

# --- Flat: basic struct inference ---

run_test "flat: 3 uniform objects get struct type" \
    '{"items":[{"a":1,"b":2},{"a":3,"b":4},{"a":5,"b":6}]}' \
    "items"

run_test "flat: field labels present" \
    '{"items":[{"x":1,"y":2},{"x":3,"y":4},{"x":5,"y":6}]}' \
    "x"

run_test "flat: string fields get labels" \
    '{"records":[{"name":"a","val":"b"},{"name":"c","val":"d"},{"name":"e","val":"f"}]}' \
    "name"

# --- Below threshold: 2 objects should NOT be inferred ---

# with no inference, inner objects keep quoted keys like "a":
run_test "below threshold: 2 objects not inferred" \
    '{"items":[{"a":1,"b":2},{"a":3,"b":4}]}' \
    '"a":'

# --- Nested: structs inside structs ---

run_test "nested: outer array has struct type" \
    '{"outer":[{"id":1,"inner":[{"x":10,"y":20},{"x":30,"y":40},{"x":50,"y":60}]},{"id":2,"inner":[{"x":70,"y":80},{"x":90,"y":100},{"x":110,"y":120}]},{"id":3,"inner":[{"x":130,"y":140},{"x":150,"y":160},{"x":170,"y":180}]}]}' \
    "outer"

run_test "nested: inner array fields get labels" \
    '{"outer":[{"id":1,"inner":[{"x":10,"y":20},{"x":30,"y":40},{"x":50,"y":60}]},{"id":2,"inner":[{"x":70,"y":80},{"x":90,"y":100},{"x":110,"y":120}]},{"id":3,"inner":[{"x":130,"y":140},{"x":150,"y":160},{"x":170,"y":180}]}]}' \
    "x"

# --- Nested: inner struct inferred even when outer doesn't qualify ---

run_test "nested: inner struct only (outer has non-primitive fields)" \
    '{"data":{"deep":[{"p":1,"q":2},{"p":3,"q":4},{"p":5,"q":6}]}}' \
    "deep"

run_test "nested: inner struct field labels" \
    '{"data":{"deep":[{"p":1,"q":2},{"p":3,"q":4},{"p":5,"q":6}]}}' \
    "p"

# --- Multiple independent struct types at different depths ---

run_test "multi-type: first struct labeled" \
    '{"a":[{"x":1,"y":2},{"x":3,"y":4},{"x":5,"y":6}],"b":[{"m":"aa","n":"bb"},{"m":"cc","n":"dd"},{"m":"ee","n":"ff"}]}' \
    "a"

run_test "multi-type: second struct labeled" \
    '{"a":[{"x":1,"y":2},{"x":3,"y":4},{"x":5,"y":6}],"b":[{"m":"aa","n":"bb"},{"m":"cc","n":"dd"},{"m":"ee","n":"ff"}]}' \
    "b"

run_test "multi-type: second struct fields" \
    '{"a":[{"x":1,"y":2},{"x":3,"y":4},{"x":5,"y":6}],"b":[{"m":"aa","n":"bb"},{"m":"cc","n":"dd"},{"m":"ee","n":"ff"}]}' \
    "m"

# --- Deeply nested: 3 levels ---

run_test "3-level: deepest struct detected" \
    '{"l1":[{"l2":[{"l3":[{"v":1,"w":2},{"v":3,"w":4},{"v":5,"w":6}]}]},{"l2":[{"l3":[{"v":7,"w":8},{"v":9,"w":10},{"v":11,"w":12}]}]},{"l2":[{"l3":[{"v":13,"w":14},{"v":15,"w":16},{"v":17,"w":18}]}]}]}' \
    "v"

run_test "3-level: deepest struct w field" \
    '{"l1":[{"l2":[{"l3":[{"v":1,"w":2},{"v":3,"w":4},{"v":5,"w":6}]}]},{"l2":[{"l3":[{"v":7,"w":8},{"v":9,"w":10},{"v":11,"w":12}]}]},{"l2":[{"l3":[{"v":13,"w":14},{"v":15,"w":16},{"v":17,"w":18}]}]}]}' \
    "w"

# --- Size comparison: inferred types should be smaller ---

run_test_smaller "size: flat struct saves space" \
    '{"items":[{"a":1,"b":2,"c":3},{"a":4,"b":5,"c":6},{"a":7,"b":8,"c":9},{"a":10,"b":11,"c":12},{"a":13,"b":14,"c":15},{"a":16,"b":17,"c":18},{"a":19,"b":20,"c":21},{"a":22,"b":23,"c":24},{"a":25,"b":26,"c":27},{"a":28,"b":29,"c":30}]}'

run_test_smaller "size: nested structs save space" \
    '{"outer":[{"id":1,"inner":[{"x":10,"y":20},{"x":30,"y":40},{"x":50,"y":60}]},{"id":2,"inner":[{"x":70,"y":80},{"x":90,"y":100},{"x":110,"y":120}]},{"id":3,"inner":[{"x":130,"y":140},{"x":150,"y":160},{"x":170,"y":180}]}]}'

# --- Edge: mixed primitive types across elements ---

run_test "mixed types: uint field label present" \
    '{"pts":[{"x":1,"y":200},{"x":3,"y":400},{"x":5,"y":600}]}' \
    "x"

# --- Edge: non-uniform keys should NOT be inferred ---

# non-uniform keys: inner objects keep quoted keys (no struct inference)
run_test "non-uniform keys: no struct inference" \
    '{"items":[{"a":1,"b":2},{"a":3,"c":4},{"a":5,"b":6}]}' \
    '"a":'

# --- Summary ---

echo ""
echo "$PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ]
