#!/usr/bin/env qore
# -*- mode: qore; indent-tabs-mode: nil -*-

%new-style
%enable-all-warnings
%require-types
%strict-args

%requires ../../../../qlib/Util.qm
%requires ../../../../qlib/QUnit.qm
%requires ../../../../qlib/DataProvider
%requires ../../../../qlib/SqlUtil
%requires ../../../../qlib/Mapper.qm
%requires ../../../../qlib/TableMapper.qm

%exec-class Main

# unused, but required for "select hash" argument
const SELECT_HASH = (
    "columns": ("a", "b"),
    "where": ("a": 1),
);

const MAPV = (
    "mapped_a": "a",
    "mapped_b": "b",
    "created_c": ("code": *string sub (*any v, hash<auto> rec) { return sprintf("%n - %n", rec.a, rec.b); }),
    "constant_d": ("constant" : 1),
    "runtime": ("runtime": "rt"),
);

const INPUT = (
    ("a": 1, "b": 1),
    ("a": 1, "b": 2),
);

const OUTPUT = (
    ("mapped_a": 1, "mapped_b": 1, "created_c": "1 - 1", "constant_d": 1, "runtime": "rtv"),
    ("mapped_a": 1, "mapped_b": 2, "created_c": "1 - 2", "constant_d": 1, "runtime": "rtv"),
);

# just return INPUTs as it would be returned by SQLStatement
class FakeSQLStatement inherits Qore::SQL::AbstractSQLStatement {
    private {
        int m_ix = 0;
    }

    bool next() {
        return m_ix < 2;
    }

    bool valid() { return True; }

    hash<auto> getValue() {
        hash<auto> ret = INPUT[m_ix];
        m_ix++;
        return ret;
    }

    private list<auto> getAdvancePtr(int rows) {
        list<auto> l = INPUT;

        if (m_ix)
            splice l, 0, m_ix;

        if (rows >= 0)
            splice l, rows;

        m_ix += l.size();

        return l;
    }

    list<auto> fetchRows(softint rows = -1) {
        return getAdvancePtr(rows);
    }

    hash<auto> fetchColumns(softint rows = -1) {
        hash<auto> h = map {$1: ()}, INPUT[0].keyIterator();
        map (map h{$1.key} += $1.value, $1.pairIterator()), getAdvancePtr(rows);
        return h;
    }

    bindArgs(softlist<auto> vargs) {}

    commit() {}
    rollback() {}

    prepare(string s, ...) {}
    execArgs(list<auto> l) {}
    hash<auto> getOutput() {}
    getSQL() {}

    hash<auto> describe() {
        return {};
    }

    nothing execArgs(softlist<auto> vargs) { throw "X"; }
    hash<auto> getOutputRows() { throw "X"; }
    nothing prepareRaw(string sql) { throw "X"; }
    bool active() { throw "X"; }
    nothing bindPlaceholders(...) { throw "X"; }
    *hash<auto> fetchRow() { throw "X"; }
    nothing define() { throw "X"; }
    bool currentThreadInTransaction() { throw "X"; }
    nothing bindValues(...) { throw "X"; }
    int affectedRows() { throw "X"; }
    nothing bind(...) { throw "X"; }
    nothing bindValuesArgs(softlist<auto> vargs) { throw "X"; }
    nothing close() { throw "X"; }
    nothing exec(...) { throw "X"; }
    nothing beginTransaction() { throw "X"; }
    nothing bindPlaceholdersArgs(softlist<auto> vargs) { throw "X"; }
    auto memberGate(string m) { }
    auto methodGate(string m) { }}

# dummy datasource-like class for FakeAbstractTable
class FakeDatasource inherits Qore::SQL::AbstractDatasource {
    *string getPassword() {}
    *string getOSEncoding() {}
    *string getDBName() {}
    any selectRows(string sql, ...) {}
    bool inTransaction() { return False;}
    any getClientVersion() {}
    any exec(string sql, ...) {}
    *int getPort() {}
    any execRaw(string sql) {}
    any getServerVersion() {}
    *string getHostName() {}
    string getDriverName() { return ""; }
    any select(string sql, ...) {}
    *string getDBEncoding() { }
    any vselect(string sql, *softlist<auto> vargs) {}
    string getConfigString() { return ""; }
    any vselectRows(string sql, *softlist<auto> vargs) {}
    nothing beginTransaction() {}
    nothing rollback() {}
    any selectRow(string sql, ...) {}
    hash<auto> getConfigHash() {}
    nothing commit() {}
    any vselectRow(string sql, *softlist<auto> vargs) {}
    any vexec(string sql, *softlist<auto> vargs) {}
    *string getUserName() {}
    bool currentThreadInTransaction() { return False; }
    AbstractSQLStatement getSQLStatement() { return new FakeSQLStatement(); }
}

# Actually we don't need real table. Just a FakeSQLStatement returned from getStatement()
class FakeAbstractTable inherits SqlUtil::AbstractTable {
    public {
        FakeDatasource fds();
    }
    constructor() : SqlUtil::AbstractTable(fds, "foo_table") {}
    AbstractIterator getRowIterator(hash<auto> h, *reference<string> sql) { return new FakeSQLStatement(); }
    AbstractIterator getStatement(hash<auto> h, *reference<string> sql) { return new FakeSQLStatement(); }
    AbstractIterator getStatementNoExec(hash<auto> h, *reference<string> sql) { return new FakeSQLStatement(); }

    string getRenameSqlImpl(string new_name) {}
    AbstractPrimaryKey getPrimaryKeyImpl(any a) {}
    Constraints getConstraintsImpl(any a)  { return new Constraints(); }
    AbstractCheckConstraint addCheckConstraintImpl(string cname, string src, *hash<auto> opt) {}
    nothing setupTableImpl(hash<auto> desc, *hash<auto> opt) {}
    bool uniqueIndexCreatesConstraintImpl(any a) {}
    ForeignConstraints getForeignConstraintsImpl(*hash<auto> opt) {}
    bool checkExistenceImpl(any a) {}
    AbstractPrimaryKey addPrimaryKeyImpl(string cname, hash<auto> ch, *hash<auto> opt) {}
    AbstractColumn addColumnImpl(string cname, hash<auto> opt, bool nullable = True) {}
    Indexes getIndexesImpl(any a) {}
    AbstractForeignConstraint addForeignConstraintImpl(string cname, hash<auto> ch, string table, hash<auto> tch, *hash<auto> opt) {}
    Columns describeImpl() {}
    AbstractTrigger addTriggerImpl(string tname, string src, *hash<auto> opt) {}
    AbstractIndex addIndexImpl(string iname, bool enabled, hash<auto> ch, *hash<auto> opt) {}
    *list<auto> getAlignSqlImpl(AbstractTable t, *hash<auto> opt) {}
    Triggers getTriggersImpl(any a) {}
    string getCreateTableSqlImpl(*hash<auto> opt) {}
    bool tryInsertImpl(string sql, hash<auto> row) {}
    bool supportsTablespacesImpl(any a) {}
    *string getSqlValueImpl(any v) {}
    AbstractUniqueConstraint addUniqueConstraintImpl(string cname, hash<auto> ch, *hash<auto> opt) {}
    hash<auto> getQoreTypeMapImpl(any a) {}
    *hash<auto> doReturningImpl(hash<auto> opt, reference<string> sql, list<auto> args) {}
    hash<auto> getTypeMapImpl(any a) {}
    bool constraintsLinkedToIndexesImpl(any a) {}
    bool emptyImpl(any a) {}
    *list<auto> getCreateMiscSqlImpl(*hash<auto> opt, bool cache) {}
    string getCreateSqlImpl(list<auto> l) {}
    bool hasArrayBind(any a) {}
    nothing doSelectLimitOnlyUnlockedImpl(reference<hash<QueryInfo>> info, reference<string> sql) {}
    nothing copyImpl(AbstractTable old) {}
    nothing doSelectOrderByWithOffsetSqlUnlockedImpl(reference<hash<QueryInfo>> info, reference<string> sql, list<auto> coll) {}
    AbstractDataProviderType getNumericTypeImpl(string type_name, bool nullable, *hash<auto> options) {}
    AbstractSavepointHelper getSavepointHelperImpl(*string savepoint) {}
    bool isDuplicateRowErrorImpl(hash<::Qore::ExceptionInfo> ex) { return False; }
}

class Main inherits QUnit::DependencyInjectedTest {
    constructor() : DependencyInjectedTest("SqlStatementOutboundMapper", "1.0") {
        addTestCase("SqlStatementOutboundMapper iterator", \testIterator());
        addTestCase("SqlStatementOutboundMapper hash rows", \testHashRows());
        addTestCase("SqlStatementOutboundMapper context", \testContext());

        set_return_value(main());
    }

    private string getScriptPathImpl() {
        return get_script_path();
    }

    private performInjectionsImpl() {
        injectUserModule("../../../../qlib/TableMapper.qm");
    }

    private performCommonInjections(Program p) {
        p.importClass("FakeSQLStatement", "Qore::SQL::SQLStatement", True);
    }

    private testIterator() {
        FakeAbstractTable t();
        SqlStatementOutboundMapper m(t, SELECT_HASH, MAPV, ("runtime": ("rt": "rtv")));
        AbstractIterator it = m.iterator();

        int i = 0;
        while (it.next()) {
            hash<auto> row = it.getValue();
            testAssertion(sprintf("Row verification: %d", i), \equals(), (row, OUTPUT[i]));
            i++;
        }
    }

    private testHashRows() {
        FakeAbstractTable t();
        SqlStatementOutboundMapper m(t, SELECT_HASH, MAPV, ("runtime": ("rt": "rtv")));
        list<auto> l = ();
        while (*list<auto> tl = m.getDataRows()) {
            l += tl;
        }
        assertEq(OUTPUT, l, "getDataRows()");
    }

    private testContext() {
        FakeAbstractTable t();
        SqlStatementOutboundMapper m(t, SELECT_HASH, MAPV, ("runtime": ("rt": "rtv")));
        list<auto> l = ();
        while (*hash<auto> th = m.getData()) {
            map l += $1, th.contextIterator();
        }
        assertEq(OUTPUT, l, "getData()");
    }
}
