package org.basex.query.expr;

import static org.basex.query.QueryError.*;
import static org.basex.query.func.Function.*;
import static org.junit.jupiter.api.Assertions.*;

import org.basex.*;
import org.basex.query.func.*;
import org.basex.query.util.hash.*;
import org.basex.query.value.item.*;
import org.basex.query.value.map.*;
import org.basex.query.value.seq.*;
import org.junit.jupiter.api.*;

/**
 * Tests for XQuery records.
 *
 * @author BaseX Team, BSD License
 * @author Gunther Rademacher
 */
public final class RecordTest extends SandboxTest {
  /** Empty record. */
  @Test public void instanceOf() {
    query("declare record x(x); { 'x': () } instance of x", true);

    query("{} instance of record()", true);
    query("{ 'x': () } instance of record()", false);
    query("declare record local:empty(); {} instance of local:empty", true);
    query("declare record local:empty(); { 'x': () } instance of local:empty", false);

    query("{ 'x': () } instance of record(x)", true);
    query("{} instance of record(x)", false);

    query("{ 'x': (), 'y': () } instance of record(x, y?)", true);
    query("{ 'x': (), 'y': () } instance of record(x)", false);
    query("{ 'x': (), 0: () } instance of record(x, y?)", false);
    query("{ 'x': (), 0: () } instance of record(x)", false);

    query("declare record local:coord(x, y); "
        + "let $coord := local:coord(1, 2) "
        + "let $new := map:remove($coord, 'x') "
        + "return $new instance of local:coord", false);

    query("map:entries({ 'a': 1 }) instance of record(a)", true);
    query("map:entries({ 'x': 1 }) instance of record(a)", false);
    query("let $map := (\n"
        + "  {'x':5, 'y':6}\n"
        + "  => map:put(xs:NCName('x'), true())\n"
        + "  => map:put(xs:NCName('y'), (false(), false()))\n"
        + ")\n"
        + "return (\n"
        + "  $map instance of map(xs:NCName, xs:boolean+) or\n"
        + "  $map instance of map(xs:string, xs:boolean+)\n"
        + ")", true);
  }

  /** Recursive records. */
  @Test public void recRec() {
    query("declare variable $v as list := "
        + "{ 'value': 42, 'next': { 'value': 43, 'next': { 'value': 44 } } };\n"
        + "declare record list(value as item()*, next? as list);\n"
        + "$v",
        "{\"value\":42,\"next\":{\"value\":43,\"next\":{\"value\":44}}}");
    query("declare variable $v := "
        + "  { 'value': 42, 'next': { 'value': 43, 'next': { 'value': 44 } } } instance of list;\n"
        + "declare record list(value as item()*, next? as list);\n"
        + "$v",
        true);
    query("declare variable $v := "
        + "  { 'value': 42, 'next': { 'value': 43, 'next': { 'value': 44, 'next': () } } } "
        + "instance of list;\n"
        + "declare record list(value as item()*, next? as list);"
        + "$v",
        false);
    // recursive RecordType.instanceOf
    query("declare record list1(value, next? as list1);\n"
        + "declare record list2(value, next? as list2);\n"
        + "fn($l as list2) as list1 { $l }({ 'value': () })",
        "{\"value\":()}");
    // recursive RecordType.eq and RecordType.instanceOf
    query("declare record list1(value, next? as list1);\n"
        + "declare record list2(value, next? as list2);\n"
        + "declare function local:f1($l as list1) as list2 { $l };\n"
        + "declare function local:f2($f as fn(list2) as list1, $l as list1) as list2 { $f($l) };\n"
        + "local:f2(local:f1#1, { 'value': () })",
        "{\"value\":()}");
    // recursive RecordType.eq and RecordType.instanceOf
    query("declare function local:f2($f as fn(list2) as list1, $l as list1) as list2 { $f($l) };\n"
        + "declare record list1(value, next? as list1);\n"
        + "declare record list2(value, next? as record(value, next? as list2));\n"
        + "local:f2(fn($l as list1) as list2 { $l }, { 'value': 42, 'next': { 'value': 43 } })",
        "{\"value\":42,\"next\":{\"value\":43}}");
    // recursive RecordType.union
    query("declare record list1(value, next? as list1);declare record list2(item, next? as list2);"
        + "fn($l1 as list1, $l2 as list2) { "
        + "map:merge(($l1, $l2))}("
        + "{ 'value': 42, 'next': { 'value': 43 } }, { 'item': 44,'next': { 'item': 45 } })",
        "{\"value\":42,\"next\":{\"value\":43},\"item\":44}");
    // recursive RecordType.intersect
    query("declare record list1(next? as list1, x, y?);\n"
        + "declare record list2(next? as list2, x, z?);\n"
        + "let $f := fn($r as record(next? as list2, x as xs:boolean)) as xs:boolean { $r?x }\n"
        + "let $r as record(next? as list1, x as xs:untypedAtomic) := { 'x': <a>0</a> }\n"
        + "return $f($r)",
        "false");

    //  cannot convert empty-sequence() to optional list
    error("declare variable $v as list :=\n"
        + "  { 'value':42, 'next': { 'value':43, 'next': { 'value': 44, 'next':() } } };\n"
        + "declare record list(value as item()*, next? as list);\n"
        + "$v",
        INVTYPE_X);
    // recursive RecordType.eq and RecordType.instanceOf
    error("declare function local:f2($f as fn(list2) as list1, $l as list1) as list2 { $f($l) };\n"
        + "declare record list1(value, next? as list1);\n"
        + "declare record list2(value, next? as record(value as xs:string, next? as list2));\n"
        + "local:f2(fn($l as list1) as list2 { $l }, { 'value': 42, 'next': { 'value': 43 } })",
        INVTYPE_X);
  }

  /** Record constructor function. */
  @Test public void recConstr() {
    query("declare namespace cx = 'CX';\n"
        + "declare record cx:complex(r as xs:double, i as xs:double := 0);\n"
        + "cx:complex(3, 2), cx:complex(3)",
        "{\"r\":3,\"i\":2}\n{\"r\":3,\"i\":0}");
    query("declare namespace cx = 'CX';\n"
        + "declare record cx:complex(r as xs:double, i? as xs:double);\n"
        + "cx:complex(3, 2), cx:complex(3)",
        "{\"r\":3,\"i\":2}\n{\"r\":3}");
    query("declare namespace cx = 'CX';\n"
        + "declare record cx:complex(r as xs:double, i? as xs:double? := ());\n"
        + "cx:complex(3, 2), cx:complex(3)",
        "{\"r\":3,\"i\":2}\n{\"r\":3,\"i\":()}");
    query("declare namespace p = 'P'\n;"
        + "declare record p:person(first as xs:string, last as xs:string);\n"
        + "p:person('John', 'Smith')",
        "{\"first\":\"John\",\"last\":\"Smith\"}");
    // recursive record type constructor function
    query("declare function local:f($x, $y) {local:list($x, $y)};\n"
        + "declare record local:list (value as item()*, next? as local:list);\n"
        + "local:f(42, local:f(43, local:f(44, ())))",
        "{\"value\":42,\"next\":{\"value\":43,\"next\":{\"value\":44}}}");
    // function as entry in record
    query("declare namespace geom = 'GEOM';\n"
        + "declare record geom:rectangle(\n"
        + "         width as xs:double,\n"
        + "         height as xs:double,\n"
        + "         area as fn(geom:rectangle) as xs:double :=\n"
        + "            fn($this as geom:rectangle) {\n"
        + "                $this?width \u00d7 $this?height\n"
        + "            }\n"
        + ");\n"
        + "let $box := geom:rectangle(3, 2)\n"
        + "return $box?area($box)",
        6);

    // constructor via function item
    query("declare record local:r(x); local:r#1(42)", "{\"x\":42}");
    // constructor via function lookup
    query("declare record local:r(x); function-lookup(#local:r, 1)(42)", "{\"x\":42}");

    // function declaration and record constructor with the same signature
    error("""
      declare function local:f($x) { local:r($x) };
      declare function local:r($x) { 43 };
      declare record local:r(x);
      local:f(42)
      """,
      DUPLFUNC_X);
    // record constructor and function declaration with the same signature
    error("""
      declare function local:f($x) { local:r($x) };
      declare record local:r(x);
      declare function local:r($x) { 43 };
      local:f(42)
      """,
      DUPLFUNC_X);
    // invalid initializer
    error("""
      declare namespace cx = 'CX';
      declare record cx:complex(r as xs:double, i? as xs:double := ());
      cx:complex(3, 2), cx:complex(3)
      """,
      INVTYPE_X);
  }

  /** Static typing. */
  @Test public void typing() {
    check("declare record local:x(x); local:x(1)", "{\"x\":1}",
        type(StaticFuncCall.class, "local:x"));
    check("declare record local:x(x, y); local:x(1, 2)", "{\"x\":1,\"y\":2}",
        type(StaticFuncCall.class, "local:x"));
  }

  /** Type propagation when removing entries. */
  @Test public void typeRemove() {
    final Function func = _MAP_REMOVE;
    check("declare record local:x(x); local:x(1) => map:remove('x')", "{}",
        root(XQTrieMap.class));
    check("declare record local:x(x); local:x(1) => map:remove(<_>x</_>)", "{}",
        root(XQTrieMap.class));
    check("declare record local:x(x); local:x(1) => map:remove('y')", "{\"x\":1}",
        empty(func));
    check("declare record local:x(x); local:x(1) => map:remove(<_>y</_>)", "{\"x\":1}",
        empty(func));
    check("declare record local:x(x); local:x(1) => map:remove(1)", "{\"x\":1}",
        empty(func));

    check("declare record local:x(x, y?); local:x(1) => map:remove('x')", "{}",
        type(func, "record(y?)"));
    check("declare record local:x(x, y?); local:x(1) => map:remove(<_>x</_>)", "{}",
        type(func, "record(y?)"));
    check("declare record local:x(x, y?); local:x(1) => map:remove('y')", "{\"x\":1}",
        type(func, "local:x"));
    check("declare record local:x(x, y?); local:x(1) => map:remove(<_>y</_>)", "{\"x\":1}",
        type(func, "local:x"));
    check("declare record local:x(x, y?); local:x(1) => map:remove('z')", "{\"x\":1}",
        empty(func), type(StaticFuncCall.class, "local:x"));
    check("declare record local:x(x, y?); local:x(1) => map:remove(1)", "{\"x\":1}",
        empty(func));
  }

  /** Type propagation when inserting entries. */
  @Test public void typePut() {
    final Function func = _MAP_PUT;
    check("declare record local:x(x); local:x(1) => map:put('x', 2)", "{\"x\":2}",
        type(RecordSet.class, "local:x"));
    check("declare record local:x(x); local:x(1) => map:put(<_>x</_>, 2)", "{\"x\":2}",
        type(RecordSet.class, "local:x"));
    check("declare record local:x(x); local:x(1) => map:put('y', 2)", "{\"x\":1,\"y\":2}",
        type(func, "record(x, y)"));
    check("declare record local:x(x); local:x(1) => map:put(<_>y</_>, 2)", "{\"x\":1,\"y\":2}",
        type(func, "record(x, y)"));
    check("declare record local:x(x); local:x(1) => map:put(0, 0)", "{\"x\":1,0:0}",
        type(func, "map(*)"));

    check("declare record local:x(x as xs:int); local:x(1) => map:put('x', <x/>)", "{\"x\":<x/>}",
        type(RecordSet.class, "record(x)"));

    check("declare record local:x(x, y?); local:x(1) => map:put('x', 2)", "{\"x\":2}",
        type(func, "local:x"));
    check("declare record local:x(x, y?); local:x(1) => map:put(<_>x</_>, 2)", "{\"x\":2}",
        type(func, "local:x"));
    check("declare record local:x(x, y?); local:x(1) => map:put('y', 2)", "{\"x\":1,\"y\":2}",
        type(func, "local:x"));
    check("declare record local:x(x, y?); local:x(1) => map:put(<_>y</_>, 2)", "{\"x\":1,\"y\":2}",
        type(func, "local:x"));
    check("declare record local:x(x, y?); local:x(1) => map:put(0, 0)", "{\"x\":1,0:0}",
        type(func, "map(*)"));
  }

  /** Tests for the compact record map implementation. */
  @Test public void recordMap() {
    String map = "{ 'a': 1, 'b': 2 }";
    check(map + " => map:get('a')", 1, root(Itr.class));
    check(map + " => map:get('c')", "", empty());
    check(map + " => map:get(1)", "", empty());
    check(map + " => map:get(<?_ 1?> cast as xs:integer)", "", empty());
    check(map + " => map:put('b', 3)", "{\"a\":1,\"b\":3}", root(XQRecordMap.class));
    check(map + " => map:put('b', xs:byte(3))", "{\"a\":1,\"b\":3}", root(XQRecordMap.class));
    check(map + " => map:put('b', '3')", "{\"a\":1,\"b\":\"3\"}", root(XQTrieMap.class));
    check(map + " => map:put('c', 3)", "{\"a\":1,\"b\":2,\"c\":3}", root(XQTrieMap.class));
    check(map + " => map:put(3, 3)", "{\"a\":1,\"b\":2,3:3}", root(XQTrieMap.class));
    check(map + " => map:remove('b')", "{\"a\":1}", root(XQTrieMap.class));
    check(map + " => map:remove('c')", "{\"a\":1,\"b\":2}", root(XQRecordMap.class));
    check(map + " => map:remove(1)", "{\"a\":1,\"b\":2}", root(XQRecordMap.class));
    check(map + " => map:remove(<?_ 1?> cast as xs:integer)", "{\"a\":1,\"b\":2}",
        root(XQRecordMap.class));

    map = "{ 'a': 1, 'b': <?_ 2?> cast as xs:integer }";
    check(map + " => map:get('a')", 1, type(RecordGet.class, "xs:integer"));
    check(map + " => map:get('c')", "", empty());
    check(map + " => map:get(1)", "", empty());
    check(map + " => map:get(<?_ 1?> cast as xs:integer)", "", empty());
    check(map + " => map:get(<?_ a?>)", 1, type(_MAP_GET, "xs:integer?"));
    check(map + " => map:get(<?_ c?>)", "", type(_MAP_GET, "xs:integer?"));

    map = "for $b in 1 to 6 return { 'a': 1, 'b': $b }";
    check(map + " => map:get('b')", "1\n2\n3\n4\n5\n6", type(DualMap.class, "xs:integer+"));
  }

  /** Coercion of records. */
  @Test public void coercion() {
    query("let $m as record() := {} return $m", "{}");
    query("let $m as record() := { 'a': 1 } return $m", "{}");
    query("let $m as record() := { 0: 2 } return $m", "{}");

    query("let $m as record(a) := { 'a': 1 } return $m", "{\"a\":1}");
    query("let $m as record(a) := { 'a': 1, 'b': 2 } return $m", "{\"a\":1}");
    query("let $m as record(a) := { 'a': 1, 0: 2 } return $m", "{\"a\":1}");
    query("let $m as record(b) := { 'a': 1, 'b': 2 } return $m", "{\"b\":2}");
    query("let $m as record(a) := map { xs:untypedAtomic('a'): 1 } return $m", "{\"a\":1}");

    query("let $m as record(a?) := {} return $m", "{}");
    query("let $m as record(b?) := { 'a': 1 } return $m", "{}");
    query("let $m as record(a?) := map { xs:untypedAtomic('c'): 3 } return $m", "{}");

    query("let $m as record(a, b?) := { 'a': 1 } return $m", "{\"a\":1}");
    query("let $m as record(a, b?) := { 'c': 3, 'a': 1 } return $m", "{\"a\":1}");
    query("let $m as record(a?, b) := { 'b': 2 } return $m", "{\"b\":2}");
    query("let $m as record(b?, a) := { 'c': 3, 'a': 1 } return $m", "{\"a\":1}");

    query("let $m as record(a, b) := { 'a': 1, 'b': 2 } return $m", "{\"a\":1,\"b\":2}");
    query("let $m as record(a, b) := { 'b': 2, 'a': 1 } return $m", "{\"a\":1,\"b\":2}");
    query("let $m as record(b, a) := { 'a': 1, 'x': 9, 'b': 2 } return $m", "{\"b\":2,\"a\":1}");
    query("let $m as record(b, a) := { xs:untypedAtomic('a'): 1, 'b': 2 } return $m",
        "{\"b\":2,\"a\":1}");

    query("let $m as record(a?, b?) := { 'c': 3 } return $m", "{}");
    query("let $m as record(a?, b?) := { 'b': 2, 'c': 3 } return $m", "{\"b\":2}");
    query("let $m as record(a?, b?) := { 'a': 1, 'c': 3 } return $m", "{\"a\":1}");
    query("let $m as record(b?, a?) := { 'a': 1, 'b': 2, 'x': 9 } return $m", "{\"b\":2,\"a\":1}");
    query("let $m as record(a?, b?) := map { xs:untypedAtomic('c'): 3 } return $m", "{}");

    query("fn($r as record(a)) { $r } ({ 'a': 1, 'b': 2 })", "{\"a\":1}");

    error("let $m as record(a) := {} return $m", INVTYPE_X);
    error("let $m as record(b) := { 'a': 1 } return $m", INVTYPE_X);

    error("let $m as record(a?, b) := {} return $m", INVTYPE_X);
    error("let $m as record(a?, b) := { 'c': 3 } return $m", INVTYPE_X);
    error("let $m as record(a?, b) := { 'a': 1 } return $m", INVTYPE_X);
    error("let $m as record(a, b?) := {} return $m", INVTYPE_X);
    error("let $m as record(a, b?) := { 'c': 3 } return $m", INVTYPE_X);
  }

  /** Equality of map/record constructors. */
  @Test public void equal() {
    // empty record
    String constr = "{}";
    check("(" + constr + ", " + constr + ")", "{}\n{}", exists(SingletonSeq.class));

    // single-entry record (compile-time evaluation)
    constr = "{ 'AA': 0 }";
    check("(" + constr + ", " + constr + ")?AA", "0\n0", exists(SingletonSeq.class));
    // small record (compile-time evaluation)
    constr = "{ 'AA': 0, 'AB': 1 }";
    check("(" + constr + ", " + constr + ")?AA", "0\n0", exists(SingletonSeq.class));
    check("(" + constr + ", { 'AA': 0, 'AB': 2 })?AA", "0\n0", empty(SingletonSeq.class));
    // big record (compile-time evaluation)
    constr =
        "{ 'AA': 0, 'AB': 0, 'AC': 0, 'AD': 0, 'AE': 0, 'AF': 0, 'AG': 0, 'AH': 0, 'AI': 0, "
        + "'AJ': 0, 'AK': 0, 'AL': 0, 'AM': 0, 'AN': 0, 'AO': 0, 'AP': 0, 'AQ': 0, 'AR': 0, "
        + "'AS': 0, 'AT': 0, 'AU': 0, 'AV': 0, 'AW': 0, 'AX': 0, 'AY': 0, 'AZ': 0 }";
    check("(" + constr + ", " + constr + ")?AA", "0\n0", exists(SingletonSeq.class));
    // map (compile-time evaluation)
    constr =
        "{ 'AA': 0, 'AB': 0, 'AC': 0, 'AD': 0, 'AE': 0, 'AF': 0, 'AG': 0, 'AH': 0, 'AI': 0, "
        + "'AJ': 0, 'AK': 0, 'AL': 0, 'AM': 0, 'AN': 0, 'AO': 0, 'AP': 0, 'AQ': 0, 'AR': 0, "
        + "'AS': 0, 'AT': 0, 'AU': 0, 'AV': 0, 'AW': 0, 'AX': 0, 'AY': 0, 'AZ': 0, "
        + "'BA': 0, 'BB': 0, 'BC': 0, 'BD': 0, 'BE': 0, 'BF': 0, 'BG': 0 }";
    check("(" + constr + ", " + constr + ")?AA", "0\n0", exists(SingletonSeq.class));

    // single-entry record (runtime evaluation)
    constr = "{ 'AA': <a/> }";
    check("(" + constr + ", " + constr + ")?AA", "<a/>\n<a/>", exists(REPLICATE));
    // small record (runtime evaluation)
    constr = "{ 'AA': 0, 'AB': <a/> }";
    check("(" + constr + ", " + constr + ")?AA", "0\n0", exists(REPLICATE));
    check("(" + constr + ", { 'AA': 0, 'AB': <b/> })?AA", "0\n0", empty(REPLICATE));
    // big record (runtime evaluation)
    constr =
        "{ 'AA': 0, 'AB': 0, 'AC': 0, 'AD': 0, 'AE': 0, 'AF': 0, 'AG': 0, 'AH': 0, 'AI': 0, "
        + "'AJ': 0, 'AK': 0, 'AL': 0, 'AM': 0, 'AN': 0, 'AO': 0, 'AP': 0, 'AQ': 0, 'AR': 0, "
        + "'AS': 0, 'AT': 0, 'AU': 0, 'AV': 0, 'AW': 0, 'AX': 0, 'AY': 0, 'AZ': <a/> }";
    check("(" + constr + ", " + constr + ")?AA", "0\n0", exists(REPLICATE));
    // map (runtime evaluation)
    constr =
        "{ 'AA': 0, 'AB': 0, 'AC': 0, 'AD': 0, 'AE': 0, 'AF': 0, 'AG': 0, 'AH': 0, 'AI': 0, "
        + "'AJ': 0, 'AK': 0, 'AL': 0, 'AM': 0, 'AN': 0, 'AO': 0, 'AP': 0, 'AQ': 0, 'AR': 0, "
        + "'AS': 0, 'AT': 0, 'AU': 0, 'AV': 0, 'AW': 0, 'AX': 0, 'AY': 0, 'AZ': 0, "
        + "'BA': 0, 'BB': 0, 'BC': 0, 'BD': 0, 'BE': 0, 'BF': 0, 'BG': <a/> }";
    check("(" + constr + ", " + constr + ")?AA", "0\n0", exists(REPLICATE));

    // named record (runtime evaluation)
    check("declare record local:r(AA, AB); (local:r(0, <a/>), local:r(0, <a/>))?AA",
        "0\n0", exists(REPLICATE));
  }

  /** Checks that every built-in record type has a constructor function in {@link Function}. */
  @Test public void builtInRecordsHaveConstructors() {
    final QNmSet funcNames = new QNmSet();
    for(final Function f : Function.values()) {
      funcNames.add(f.definition().name);
    }
    for(final Records record : Records.values()) {
      assertTrue(funcNames.contains(record.get().name()),
          "Missing constructor function for " + record);
    }
  }
}
