

const countEach = proc callee(k,v){
    if(1===++callee.count){
        assert 'unique' === typeinfo(name v);
        assert 'string' === typeinfo(name k);
        assert this inherits enumProto;
    }
};

const enumProto = (enum {a}).prototype;

scope {
    assert 'enum' === typeinfo(name enum{a,b,c});
    assert 'CWAL_SCR_SYNTAX' === catch{enum {a,b,c, /* extra trailing comma */}}.codeString();
    assert 'CWAL_SCR_SYNTAX' === catch{enum {a,,c /* extra comma */}}.codeString();
    assert 'CWAL_SCR_SYNTAX' === catch{enum {a:,,c /* missing value after ':' */}}.codeString()
}

scope {
    const E = enum MyEnum {
        // This small enum will use an Object for storage.
        A, B, C
    };

    assert 'MyEnum' === typeinfo(name E);
    assert E inherits enumProto;
    assert 3 === E.#;
    assert E.A;
    assert undefined === E.A.prototype
        /* prototype pseudo-property is allowed but (currently) evals
	   to undefined */;
    assert E.hasEnumEntry(E.A);
    assert E.hasEnumEntry('A');
    assert !E.hasEnumEntry('a');
    assert 'C' === E[E.('C')];
    assert catch {E.x}.message.indexOf('Unknown')>=0;
    assert catch {E.A = 1}.message.indexOf('disallowed')>=0;
    var ar = E.getEnumKeys();
    assert 'array' === typeinfo(name ar);
    assert 3 === ar.length();
    assert ar.indexOf('C') >= 0;
    assert ar.indexOf('D') < 0;

    E.eachEnumEntry(countEach);
    assert 3 === countEach.count;
}

scope {
    const Big = enum {
        ☺,b,c,d,e,
        f,g,h,i,j,
        k,l,m,n,o,
        p,q,r,s,t,
        u,v,
        こんにちは
    };
    const bigLen = Big.#;
    assert 23 === bigLen;
    assert 'enum' === typeinfo(name Big);
    assert Big inherits enumProto;
    assert 'c' === Big[Big.c];
    assert '☺' === Big[Big.☺];
    assert 'こんにちは' === Big[Big.こんにちは];
    assert Big.こんにちは === Big.'こんにちは';
    assert Big[<<<X こんX + 'にちは'] === Big->'こんにちは';
    assert catch {Big.z}.message.indexOf('Unknown')>=0;
    assert catch {Big.b = 1}.message.indexOf('disallowed')>=0;

    var ar = Big.getEnumKeys();
    assert 'array' === typeinfo(name ar);
    assert bigLen === ar.length();
    assert ar.indexOf('☺') >= 0;
    assert ar.indexOf('B') < 0;

    countEach.count = 0;
    Big.eachEnumEntry(countEach);
    assert bigLen === countEach.count;
}

scope {
    var x = enum {a,b,c,d,e,f,g,h,i,j,k};
    assert 'unique' === typeinfo(name x.a);
    /**
       20191210: we may disallow enum prototype reassignment in the
       future, so don't rely on this feature. It's not a question of
       whether it makes sense to allow it, but whether or not it's a
       tolerable inconsistency vis-a-vis the disallowing of setting
       non-prototype properties. For relevant commentary, search for
       comments in s2.c near references to
       CWAL_CONTAINER_DISALLOW_PROP_SET. "The problem" is that if we
       allow it here, we always have to allow it on other "sealed"
       objects, such as the one which the s2out keyword resolves to.
    */
    x.prototype = {
        prototype: x.prototype,
        z: 3
    };
    assert 'enum' === typeinfo(name x);
    assert x.a;
    assert 3===x.z;
    assert 'CWAL_RC_DISALLOW_PROP_SET'
        === catch{x.z = 1}.codeString();
    // But...
    x.prototype.z = 4;
    assert 4===x.z /* modified in (mutable) prototype */;
    var ar = x.getEnumKeys();
    assert 'array' === typename ar;
    assert x.# === ar.length();
    assert 'CWAL_RC_NOT_FOUND'
      === catch{x['x']}.codeString();

}

scope {
   
    var x = enum {'a',b,c,d,e,f,g,h,i,j,k};
    assert 'unique' === typename x.a;
    assert 'CWAL_RC_DISALLOW_PROP_SET' === (catch x.z = 1).codeString();
    assert catch {x.z}.message.indexOf('Unknown')>=0;
    x.prototype = {
        prototype: x.prototype,
        z: 3
    };
    assert undefined === catch {x.z};
    assert 'enum' === typename x;
    assert x.a;
    assert 3===x.z;
    ++x.prototype.z;
    assert 4 === x.prototype.z;

    assert 'a' === x.a.value;
    assert 'a' === x::a;

    var e = enum { E, A: 'Aa', "B": 'abc', C: 13, OBJ: {x: 1, y:0}, D};
    assert 'enum' === typeinfo(name e);
    assert 'Aa' === e.A.value;
    assert 'abc' === e.B.value;
    assert 13 === e.C.value;
    assert 0 === e.OBJ.value.y;
    assert 1 === e.OBJ['value'].x;
    assert 1 === e.OBJ.('va'+'lue').x;
    assert 'CWAL_RC_TYPE' === catch (e.OBJ.invalid).codeString();
    assert e[e.A] === 'A';
    assert 'D' === e.D.value;
    assert 2 === e.A.value.length();
}

scope {
    const e = enum {
        a: {x:1, y:undefined},
        b: proc(){return 1},
        c,
        d
    };
    assert 4 === e.#;
    assert 1 === e.a.value.x;
    assert 1 === e::a.x;
    assert 1 === e::'a'.x;
    assert 1 === e.b.value();
    assert 1 === e::b();
    assert undefined === e.a.value.y++;
    assert e.a.value.x === e.a.value.y;
    assert 2 === ++e::a.y;
    assert 2 === e::a.y;
    assert 2 === e::(scope{while(true) break 'a'}).y
        /* reminder: parens needed b/c dot/dotdot treat all identifiers
           on the RHS equally, and does not expand keywords as keywords. */;
    assert e.c;
    assert 'c' === e::c;
    assert 'CWAL_RC_DISALLOW_PROP_SET' === (catch e.a = 1).codeString();
    assert 'CWAL_RC_NOT_FOUND' === catch{e.x}.codeString();
    assert 'CWAL_RC_NOT_FOUND' === catch {e::x}.codeString();
}

scope {
    scope {
        var e = enum {a:{x:1},b:{x:2}};
        e.a.value.b = e.b;
        e.b.value.a = e.a;
    }
    assert 1 /* must not crash during prior scope's cleanup (former
                cwal bug in cleanup handling of Unique-type values).*/;
    /* Must also properly upscope everything... */
    assert 200 === scope {
        var e = enum {a:{x:100},b:{x:200}};
        e::a.b = e.b;
        e::b.a = e.a; 
        assert e.a.value.b === e::a.b;
        assert e::a.b === e.b;
        e.a.value.b/*===e.b*/
    }.value.a/*===e.a*/.value.b.value.x;
    assert scope {
        enum {a:{x:100}}::a
    }.x === 100;
    assert enum {a:{x:-100}}::a.x === -100;
}

scope {
    const o2e = proc(obj, name) {
        affirm typeinfo(iscontainer obj);
        var b = new s2.Buffer(100).append("enum ", name ||| '', '{'),
            first = true;
        foreach(obj=>k){
            first ? first = false : b.append(", ");
            b.append(k , ": obj.",k);
        }
        return b.append("}").evalContents(__FLC);
    };
    var obj = {a:1, b:{x:2}};
    var e = o2e(obj, 'blah');
    assert 'blah' === typeinfo(name e);
    assert 2 === e.#;
    //print(__FLC, e);
    foreach(obj=>k, v) {
        assert e::(k) === v;
        assert e->(k).value === v;
        assert e.(k).value === v;
        assert v === ('e.'+k+'.value').evalContents(__FLC);
        assert v === ('e::'+k).evalContents(__FLC);
    }
    
    assert enum{a:enum{x:30}}::a::x + enum{b:30}::b === 60 /* lifetime(s) check */;
}

scope {
    const e = enum {
      f1: proc(){assert 'enum' === typeinfo(name E)},
      f2: proc(){assert 'unique' === typeinfo(name E.f1)}
    };
    const imports = {E:e};
    foreach(e=>k,v) v.value.importSymbols(imports);
    e::f1();
    e::f2();
    e::f1.z = e.f1
        /* will propagate e.f1 out of the scope. Formerly this crashed
           during scope cleanup due to a mis-assertion in the Unique-type's
           finalizer. Let's see what happens to it... */;
}

scope {
    /* 20191210: it's now possible (though probably not wise) to set
       the prototype property in an enum's body. Before this, that
       handling was not well-defined. */
    const x = {__typename: "X"};
    assert catch {enum {prototype:x}}.message.indexOf("at least one") > 0;
    var e = enum {prototype:x, a};
    assert 'X' === typeinfo(name e);
    assert e inherits x;
    assert typeinfo(isenum e);
    e = enum eee {prototype:x, b, c};
    assert 'eee' === typeinfo(name e);
    assert e inherits x;
    assert typeinfo(isenum e);

    e = enum hhh {prototype:x, b, c, d, e, f, g, h, i, j, k};
    assert 'hhh' === typeinfo(name e);
    assert e inherits x;
    assert e.prototype === x;
    assert typeinfo(isenum e);

    e = enum { prototype: null /* remove prototype */, a };
    assert catch {e.prototype}.message.indexOf('Unknown')===0;
    /**
       Interestingly, this formulation doesn't work:

       assert catch e.prototype.message.indexOf('Unknown')===0;

       Unexpected consecutive non-operators:
         #3006 (KeywordCatch) ==> #1011 (Identifier)

       At the ".message" part. Because... ???

       Hmmm.
    */

    ;;; /* <=== for vacuum testing */
}

scope {
    /* As of 2020-02-21, enums are always hashes, but the the
       restriction against using the hash search operator persists
       because it would otherwise behave exactly as the -> operator.
       typeinfo(ishash) still evals to false for enums.
    */
    const e = enum{a};
    assert typeinfo(isunique e.a);
    assert 'a' === e::a;
    assert e.a === e->'a';
    assert catch {e#'a'}.codeString()==='CWAL_RC_TYPE' /* hash op is not allowed on enums */;
    assert e::a === e.'a'.value;
    assert typeinfo(is-enum e);
    assert !typeinfo(is-hash e);
}

scope {
    /* Examples from the user docs... */
    const e = enum OptionalTypeName {
        a, // its own name (a string) as its value
        b: 2, // any value type is fine
        f: proc(){return this}
    };
    assert 3 === e.#;
    assert 'a' === e.a.value;
    assert 'a' === e::a; // equivalent
    assert 2 === e.b.value;
    assert 2 === e::b; // equivalent unless the entry.value is a Function call:
    assert e.f === e.f.value(); // e.f is bound as 'this'
    assert e::f === e::f(); // no 'this', so e.f.value is its own 'this'
    assert 'b' === e[e.b]; // get the string-form name of an entry
    assert e.a === e['a']; // note that e['a'] is functionally identical to e.a.
    assert 'OptionalTypeName' === typeinfo(name e);
    assert undefined === e->'c'; //-> op does not throw for unknown properties
    assert catch {e#'c'}.codeString()==='CWAL_RC_TYPE'; // hash op not allowed
    assert catch {e::c}.codeString()==='CWAL_RC_NOT_FOUND'; // unknown property
    assert catch {e.c}.codeString()==='CWAL_RC_NOT_FOUND'; // unknown property
    assert catch {e.a = 1}.codeString()==='CWAL_RC_DISALLOW_PROP_SET';
}
