#include <bu/sio.h>
#include <bu/json.h>
#include <bu/process.h>
#include <bu/optparser.h>
#include <bu/blob.h>
#include <bu/blobbuilder.h>
#include <bu/file.h>

#include <sys/types.h>
#include <dirent.h>

typedef Bu::List<Bu::Blob> BlobList;

Bu::Blob getSuiteName( const Bu::Blob &bSuiteExec )
{
    Bu::Process proc(
        Bu::Process::StdOut,
        bSuiteExec.getData(),
        bSuiteExec.getData(),
        "--print-name",
        NULL
        );

    Bu::String sResult = proc.readAll().trimWhitespace();
    Bu::Blob bRet( sResult.getStr(), sResult.getSize() );
    return bRet;
}

Bu::Json *runSuite( const Bu::Blob &bSuiteExec, Bu::Json *pSuiteStatus=NULL )
{
    BlobList lCmd;
    lCmd.append( bSuiteExec );
    lCmd.append( "--interop" );
    if( pSuiteStatus != NULL )
    {
        for( Bu::Json::iterator i = (*pSuiteStatus)["tests"].begin(); i; i++ )
        {
            lCmd.append( (**i)["name"].getString().get() );
        }
    }
    char **argv = new char*[lCmd.getSize()+1];
    int j = 0;
    for( BlobList::iterator i = lCmd.begin(); i; j++, i++ )
    {
        argv[j] = (*i).getData();
    }
    argv[lCmd.getSize()] = NULL;
    Bu::Process proc(
        Bu::Process::StdOut,
        bSuiteExec.getData(),
        argv
        );
    Bu::BlobBuilder bbReport;
    while( proc.isRunning() )
    {
        int iRead = 0;
        char dRead[4096];
        iRead = proc.read( dRead, 4096 );
        if( iRead > 0 )
        {
            bbReport.append( dRead, iRead );
        }
    }
    delete[] argv;
    Bu::Json *pReport = new Bu::Json();
    pReport->parse( bbReport.getBlob() );
    return pReport;
}

BlobList getSuitePaths( const Bu::Blob &bDir )
{
    BlobList lPaths;
    DIR *dir = opendir( bDir.getData() );
    if( dir == NULL )
    {
        Bu::println("Could not open directory: %1").arg( bDir );
        return lPaths;
    }
    struct dirent *de = NULL;
    while( (de = readdir( dir )) != NULL )
    {
        if( de->d_type != DT_REG )
            continue;

        Bu::BlobBuilder bbPath;
        bbPath.append( bDir );
        bbPath.append("/");
        bbPath.append( de->d_name );
        Bu::Blob bPath = bbPath.getBlob();
        if( access( bPath.getData(), X_OK ) != 0 )
            continue;

        lPaths.append( bPath );
    }
    closedir( dir );

    return lPaths;
}

int main( int argc, char *argv[] )
{
    Bu::Blob bDir("unit");
    bool bAll = false;
    bool bHasStatus = false;
    Bu::Json jsLastStatus;

    Bu::OptParser options;
    options.addOption( bDir, 'p', "path", "Directory to look for unit tests.");
    options.addOption( bAll, 'a', "all", "Execute all tests, ignoring state.");
    options.setOverride("all", "true");
    options.addHelpOption();
    options.parse( argc, argv );

    if( !bAll )
    {
        try
        {
            Bu::String sData =
                Bu::File("rununits.status.json", Bu::File::Read )
                .readAll();
            jsLastStatus.parse( sData );
            bHasStatus = true;
            Bu::println("Current status loaded, created: %1")
                .arg( jsLastStatus["date"].getString().get() );
        }
        catch(...)
        {
        }
    }

    BlobList lPaths = getSuitePaths( bDir );
    Bu::Hash<Bu::Blob, Bu::Json *> hStatusSuitesByPath;

    if( bHasStatus )
    {
        BlobList lNewPaths;
        for( Bu::Json::iterator i = jsLastStatus["suites"].begin(); i; i++ )
        {
            hStatusSuitesByPath.insert( (**i)["path"].getString().get(), (*i) );
        }
        for( BlobList::iterator i = lPaths.begin(); i; i++ )
        {
            if( hStatusSuitesByPath.has( *i ) )
                lNewPaths.append( *i );
        }
        lPaths = lNewPaths;
    }

    int iNumLen = Bu::String("%1").arg( lPaths.getSize() ).end().getSize();
    int iMaxSuiteName = 0;
    Bu::Hash<Bu::Blob, Bu::Blob> hName;
    for( BlobList::iterator i = lPaths.begin(); i; i++ )
    {
        Bu::Blob bSuiteName = getSuiteName( *i );
        if( iMaxSuiteName < bSuiteName.getSize() )
            iMaxSuiteName = bSuiteName.getSize();
        hName.insert( *i, bSuiteName );
    }

    Bu::List<Bu::Json *> lReport;

    int iTest = 0;
    for( BlobList::iterator i = lPaths.begin(); i; i++ )
    {
        iTest++;
        Bu::Blob bSuiteName = hName.get( *i );
        Bu::print("\rRunning test suite [%1/%2]: %3")
            .arg( iTest, Bu::Fmt().width( iNumLen ).right() )
            .arg( lPaths.getSize(), Bu::Fmt().width( iNumLen ) )
            .arg( bSuiteName, Bu::Fmt().width( iMaxSuiteName ).fill(' ').left() );
        Bu::sio << Bu::sio.flush;

        Bu::Json *pSuiteStatus = NULL;
        if( bHasStatus )
        {
            pSuiteStatus = hStatusSuitesByPath.get( *i );
        }
        Bu::Json *pReport = runSuite( (*i), pSuiteStatus );
        pReport->insert("path", *i );
        int iUnexpected = 0;
        int iTotal = 0;
        iTotal = (*pReport)["tests"].getSize();
        for( int j = 0; j < iTotal; j++ )
        {
            if( !((*pReport)["tests"][j]["expected"].getString() ==
                (*pReport)["tests"][j]["result"].getString()) )
            {
                iUnexpected++;
            }
        }
        if( iUnexpected == 0 )
        {
            delete pReport;
        }
        else
        {
            lReport.append( pReport );
        }
    }
    Bu::println("\rCompleted %1 unit test suites.%2")
        .arg( lPaths.getSize(), Bu::Fmt().width( iNumLen ) )
        .arg( Bu::Blob(""), Bu::Fmt().width( iMaxSuiteName ).fill(' ').left() );

    if( lReport.getSize() == 0 )
    {
        Bu::println("\nNothing unexpected in unit tests.");
        unlink("rununits.status.json");
    }
    else
    {
        Bu::Json jStatus( Bu::Json::Object );

        time_t tNow = time( NULL );
        jStatus.insert("date", Bu::String(ctime( &tNow )).trimWhitespace() );
        jStatus.insertArray("suites");
        for( Bu::List<Bu::Json *>::iterator i = lReport.begin(); i; i++ )
        {
            Bu::println("\nUnexpected results in: %1")
                .arg( (**i)["suite"].getString().get() );
            Bu::Json &jStSuite = jStatus["suites"].appendObject();
            jStSuite.insert("name", (**i)["suite"].getString() );
            jStSuite.insert("path", (**i)["path"].getString() );
            Bu::Json &jStTests = jStSuite.insertArray("tests");

            for( Bu::Json::iterator iTest = (**i)["tests"].begin();
                    iTest; iTest++ )
            {
                if( (**iTest)["expected"].getString() ==
                    (**iTest)["result"].getString() )
                {
                    continue;
                }
                Bu::Json &jsTest = jStTests.appendObject();
                jsTest.insert("name", (**iTest)["name"].getString() );
                jsTest.insert("result", (**iTest)["result"].getString() );
                jsTest.insert("expected", (**iTest)["expected"].getString() );

                Bu::println("  %1: unexpected %2")
                    .arg( (**iTest)["name"].getString().get() )
                    .arg( (**iTest)["result"].getString().get() );
                if( (**iTest).has("fail") )
                {
                    if( (**iTest)["fail"].has("action") )
                    {
                        Bu::println("    unitTest( %1 );")
                            .arg( (**iTest)["fail"]["action"].getString().get() );
                        Bu::println("    at %1:%2")
                            .arg( (**iTest)["fail"]["file"].getString().get() )
                            .arg( (int)(**iTest)["fail"]["line"].getNumber() );
                    }
                    else if( (**iTest)["fail"].has("what") )
                    {
                        Bu::println("    Unexpected exception: %1")
                            .arg( (**iTest)["fail"]["what"].getString().get() );
                    }
                }
                else
                {
                    Bu::println("    No further details.");
                }
            }
            delete *i;
        }

        Bu::File("rununits.status.json", Bu::File::WriteNew )
            .write( jStatus.toString() );
    }
    
    return 0;
}
