#! /usr/bin/env dub
/++ dub.sdl:
    configuration "release" {
        targetType "executable"
    }
    configuration "unittest" {
        targetType "library"
        dependency "silly" version="~>1.1.1"
    }
+/
/+ unicode note:
    "noël" is "noe\u0308l"
    it should have two dots over the 'e'
    this is how it displays in urvxt, gnome-terminal, Firefox, and Brave
    if it looks like "noeI", try another terminal

    to confirm, try editing and running the examples at
    https://dlang.org/phobos/std_uni.html#.byCodePoint
+/
version (unittest) {
} else {
    void main() {
        import std.process : spawnProcess, wait;
        spawnProcess(["dub", "test", "--single", __FILE__, "--", "--threads=1"]).wait;
    }
}

@("UTF-8 length in bytes, codepoints, graphemes")
@safe unittest {
    import std.uni : byCodePoint, byGrapheme;
    import std.algorithm : count;
    enum s1 = "noël";
    enum s2 = "ë";

    assert(s1.length == 6);
    assert(s2.length == 3);

    assert(s1.byCodePoint.count == 5);
    assert(s2.byCodePoint.count == 2);

    assert(s1.byGrapheme.count == 4);
    assert(s2.byGrapheme.count == 1);
}

@("UTF-8 indexing by byte, codepoint, grapheme")
@safe unittest {
    import std.uni : byCodePoint, byGrapheme, Grapheme;
    import std.range : drop, indexed;
    import std.array : array;
    import std.algorithm : map, joiner;
    import std.encoding : index, decode;

    enum s1 = "noël";
    enum s2 = "ë";

    assert(s1[0..3] ~ s1[$-1] == "noel");
    assert(s1[2..5] == s2);

    const cbd = s1.byCodePoint.drop(3).front; // combining diaeresis
    enum s3 = "noël"d;
    assert(s3[3] == cbd); // dchars = codepoints
    assert(s1 == ['n', 'o', 'e', cbd, 'l']);
    assert("nöel" == ['n', 'o', cbd, 'e', 'l']);
    assert("nöel" == s1.byCodePoint.array.indexed([0, 1, 3, 2, 4]).array);

    // lambda to give decode a ref
    assert((s => s.decode)(s1[s1.index(3) .. $]) == cbd);

    auto g1 = Grapheme("ë");
    assert(s1.byGrapheme.drop(2).front == g1);
    assert(g1[].array == s2);
    assert("nëol" == s1.byGrapheme.array.indexed([0, 2, 1, 3]).map!(g => g[].array).joiner.array);
}

@("validating UTF")
@safe unittest {
    import std.utf : validate, UTFException;
    import std.encoding : isValid;
    import std.exception : assertThrown, assertNotThrown;

    enum s1 = "noël";
    enum s2 = "hello\247\205\257there";

    assertNotThrown!UTFException(s1.validate);
    assertThrown!UTFException(s2.validate);
    assert(s1.isValid);
    assert(!s2.isValid);
}

@("sanitizing UTF")
unittest {
    import std.encoding : sanitize, validLength;
    import std.utf : byDchar, replacementDchar;
    import std.algorithm : equal, filter;

    string s1 = "hello\247\205\257there";
    dchar[] replaced = [104, 101, 108, 108, 111, 65533, 65533, 65533, 116, 104, 101, 114, 101];

    assert("hellothere".equal(s1.byDchar.filter!(c => c != replacementDchar)));
    assert("hello" == s1[0 .. s1.validLength]);
    assert(s1.byDchar.equal(replaced));
    assert(s1.sanitize.equal(replaced));
}

@("bytes -> chars while checking UTF")
unittest {
    import std.string : representation, assumeUTF;
    import std.utf : validate, UTFException;
    import std.encoding : isValid;
    import std.exception : enforce, assertThrown;
    import core.exception : AssertError;

    immutable(ubyte)[] b1 = "noël".representation;
    immutable(ubyte)[] b2 = "hello\247\205\257there".representation;

    string s1 = b1.assumeUTF;   // only checks in debug builds!

    auto s2 = cast(char[]) b1;  // unchecked
    s2.validate;                // throws UTFException if invalid
    assert(s2.isValid);         // only checks if asserts run
    enforce!UTFException(s2.isValid, "invalid UTF");

    assertThrown!AssertError(b2.assumeUTF);
    auto s3 = cast(char[]) b2;
    assertThrown!UTFException(s3.validate);
    assert(!s3.isValid);
    assertThrown!UTFException(enforce!UTFException(s3.isValid));
}

@("byte-order-mark (BOM) ops")
unittest {
    import std.encoding : getBOM, BOM, bomTable;
    import std.string : representation;

    enum s1 = "\x84\x31\x95\x33<- GB-18030".representation;
    enum s2 = "\xEF\xBB\xBF<- UTF-8".representation;
    enum s3 = "\xFF\xFE<- UTF-16 (LE)".representation;
    enum s4 = "no BOM here".representation;

    assert(s1[s1.getBOM.sequence.length .. $] == "<- GB-18030"); // stripping
    assert(s2 == bomTable[BOM.utf8].sequence ~ "<- UTF-8".representation); // adding
    assert(BOM.utf16le == s3.getBOM.schema); // getting
    assert(s4.getBOM.schema == BOM.none); // checking
}
