/*
 * Copyright (C) 2007-2023 Xagasoft, All rights reserved.
 *
 * This file is part of the libbu++ library and is released under the
 * terms of the license contained in the file LICENSE.
 */

#include "bu/unitsuite.h"
#include "bu/file.h"
#include "bu/sio.h"
#include "bu/optparser.h"
#include "bu/json.h"
#include <stdlib.h>
#include <time.h>

using namespace Bu;

#include <unistd.h>

Bu::UnitSuite::UnitSuite() :
    iOptions( 0 ),
    pReport( NULL )
{
}

Bu::UnitSuite::UnitSuite( int iOptions ) :
    iOptions( iOptions )
{
}

Bu::UnitSuite::~UnitSuite()
{
    delete pReport;
}

int Bu::UnitSuite::run( int argc, char *argv[] )
{
    bool bCleanup = true;
    bool bInterop = false;
    OptParser p;
    p.addOption( Bu::slot( this, &Bu::UnitSuite::onListCases ), 'l', "list",
            "List available test cases." );
    p.addOption( Bu::slot( this, &Bu::UnitSuite::onPrintName ), "print-name",
            "Print the internal name of this test suite.");
    p.addOption( bCleanup, "no-cleanup", "Don't erase temp files.");
    p.setOverride( "no-cleanup", false );
    p.addOption( bInterop, "interop", "Output machine parsable json.");
    p.setOverride( "interop", true );
    p.setNonOption( Bu::slot( this, &Bu::UnitSuite::onAddTest ) );
    p.addHelpOption();
    p.parse( argc, argv );

    if( !hSelTests.isEmpty() )
    {
        TestList lSub;
        for( TestList::iterator i = lTests.begin(); i != lTests.end(); i++ )
        {
            if( hSelTests.has( (*i).sName ) )
                lSub.append( *i );
        }

        lTests = lSub;
    }

    if( bInterop )
        pReport = new ReportJson();
    else
        pReport = new ReportConsole();
    pReport->suiteStarting( *this, lTests );

    for( TestList::iterator i = lTests.begin(); i != lTests.end(); i++ )
    {
        try
        {
            iStepCount = -1;
            iProgress = 0;
            if( pReport )
                pReport->testStarting( *i );
            (this->*(i->fTest))();
            if( pReport )
                pReport->testEnded( *i );
        }
        catch( Failed &e )
        {
            if( pReport )
                pReport->testEnded( *i, e );

            if( (iOptions & optStopOnError) )
            {
                return 0;
            }
        }
        catch( std::exception &e )
        {
            if( pReport )
                pReport->testException( *i, e );

            if( (iOptions & optStopOnError) )
            {
                return 0;
            }
        }
        catch( ... )
        {
            sio << "fail with external exception." << sio.nl;
            return -1;

            if( (iOptions & optStopOnError) )
            {
                return 0;
            }
        }
    }

    if( pReport )
        pReport->suiteEnded();

    if( bCleanup )
    {
        for( StrList::iterator i = lFileCleanup.begin(); i; i++ )
        {
            unlink( (*i).getStr() );
        }

        cleanup();
    }

    return 0;
}

Bu::File Bu::UnitSuite::tempFile( Bu::String &sFileName )
{
    Bu::File f = Bu::File::tempFile( sFileName );
    lFileCleanup.append( sFileName );
    return f;
}

void Bu::UnitSuite::add( Test fTest, const Bu::String &sName, Expect e )
{
    TestInfo ti;
    ti.sName = sName;
    ti.eExpect = e;
    long index = ti.sName.rfindIdx("::");
    if( index != -1 )
    {
        String tmp = sSuiteName;
        tmp += ti.sName.getStr()+index;
        ti.sName = tmp;
    }
    ti.fTest = fTest;
    lTests.append( ti );
}

void Bu::UnitSuite::setName( const String &sName )
{
    sSuiteName = sName;
}

Bu::String Bu::UnitSuite::getName() const
{
    return sSuiteName;
}

void Bu::UnitSuite::cleanup()
{
}

void Bu::UnitSuite::dispProgress()
{
    if( tLastUpdate == time( NULL ) )
        return;
    if( pReport )
        pReport->updateProgress( iProgress, iStepCount );
    tLastUpdate = time( NULL );
}

void Bu::UnitSuite::setStepCount( int iSteps )
{
    iStepCount = iSteps;
    if( iStepCount < 0 )
        return;
    tLastUpdate = 0;
    dispProgress();
}

void Bu::UnitSuite::incProgress( int iAmnt )
{
    iProgress += iAmnt;
    if( iProgress < 0 )
        iProgress = 0;
    if( iProgress > iStepCount )
        iProgress = iStepCount;
    dispProgress();
}

void Bu::UnitSuite::setProgress( int iAmnt )
{
    iProgress = iAmnt;
    if( iProgress < 0 )
        iProgress = 0;
    if( iProgress > iStepCount )
        iProgress = iStepCount;
    dispProgress();
}

int Bu::UnitSuite::onListCases( StrArray )
{
    sio << "Test cases:" << sio.nl;
    for( TestList::iterator i = lTests.begin(); i; i++ )
    {
        sio << "\t- " << Fmt( iNameWidth, 10, Fmt::Left ) << (*i).sName << "  "
            << (*i).eExpect << sio.nl;
    }
    sio << sio.nl;
    exit( 0 );
    return 0;
}

int Bu::UnitSuite::onPrintName( StrArray )
{
    Bu::println("%1").arg( sSuiteName );
    exit( 0 );
    return 0;
}

int Bu::UnitSuite::onAddTest( StrArray aParam )
{
    hSelTests.insert( aParam[0], true );
    return 0;
}

Bu::Formatter &Bu::operator<<( Bu::Formatter &f, const Bu::UnitSuite::Expect &e )
{
    switch( e )
    {
        case Bu::UnitSuite::expectPass:
            return f << "pass";

        case Bu::UnitSuite::expectFail:
            return f << "fail";
    }

    return f << "**error**";
}

/////////
// Bu::UnitSuite::Report
////

Bu::UnitSuite::Report::Report()
{
}

Bu::UnitSuite::Report::~Report()
{
}

/////////
// Bu::UnitSuite::ReportConsole
////

Bu::UnitSuite::ReportConsole::ReportConsole() :
    iTestCount( 0 ),
    iNameWidth( 0 ),
    iEPass( 0 ),
    iEFail( 0 ),
    iUPass( 0 ),
    iUFail( 0 )
{
}

Bu::UnitSuite::ReportConsole::~ReportConsole()
{
}

void Bu::UnitSuite::ReportConsole::suiteStarting( const UnitSuite & /*rSuite*/,
        const TestList &lTests )
{
    iTestCount = lTests.getSize();
    for( TestList::const_iterator i = lTests.begin(); i; i++ )
    {
        if( iNameWidth < i->sName.getSize() )
            iNameWidth = i->sName.getSize();
    }
}

void Bu::UnitSuite::ReportConsole::testStarting( const TestInfo &rTest )
{
    sio << Fmt( iNameWidth+3, Fmt::Left ).fill('.') << rTest.sName
        << sio.flush;
}

void Bu::UnitSuite::ReportConsole::updateProgress( int iProgress,
        int iStepCount )
{
    sio << Fmt(3) << (iProgress*100/iStepCount) << "%" << "\b\b\b\b"
        << sio.flush;
}

void Bu::UnitSuite::ReportConsole::testEnded( const TestInfo &rTest )
{
    switch( rTest.eExpect )
    {
        case expectPass:
            sio << "pass." << sio.nl;
            iEPass++;
            break;

        case expectFail:
            sio << "unexpected pass." << sio.nl;
            iUPass++;
            break;
    }
}

void Bu::UnitSuite::ReportConsole::testEnded( const TestInfo &rTest,
        const Bu::UnitSuite::Failed &rFail )
{
    switch( rTest.eExpect )
    {
        case expectPass:
            sio << "unexpected ";
            iUFail++;
            break;

        case expectFail:
            sio << "expected ";
            iEFail++;
            break;
    }
    if( rFail.bFile )
    {
        sio << "fail in unitTest(" << rFail.str << "). (" << rFail.sFile
            << ":" << rFail.nLine << ")." << sio.nl;
    }
    else
    {
        sio << "fail in unitTest(" << rFail.str << ")." << sio.nl;
    }
}

void Bu::UnitSuite::ReportConsole::testException( const TestInfo &rTest,
        std::exception &e )
{
    switch( rTest.eExpect )
    {
        case expectPass:
            sio << "unexpected ";
            iUFail++;
            break;

        case expectFail:
            sio << "expected ";
            iEFail++;
            break;
    }
    sio << "fail with unknown exception.  what: " << e.what() << sio.nl;
}

void Bu::UnitSuite::ReportConsole::suiteEnded()
{
    sio << sio.nl
        << "Report:" << sio.nl
        << "\tTotal tests run:     " << iTestCount << sio.nl;

    if( iEPass > 0 )
        sio << "\tExpected passes:     " << iEPass << sio.nl;
    if( iEFail > 0 )
        sio << "\tExpected failures:   " << iEFail << sio.nl;
    if( iUPass > 0 )
        sio << "\tUnexpected passes:   " << iUPass << sio.nl;
    if( iUFail > 0 )
        sio << "\tUnexpected failures: " << iUFail << sio.nl;
    sio << sio.nl;
    if( iUPass == 0 && iUFail == 0 )
        sio << "\tNothing unexpected." << sio.nl << sio.nl;
}


/////////
// Bu::UnitSuite::Report
////

Bu::UnitSuite::ReportJson::ReportJson() :
    pReport( new Bu::Json( Bu::Json::Object ) )
{
}

Bu::UnitSuite::ReportJson::~ReportJson()
{
    delete pReport;
}

void Bu::UnitSuite::ReportJson::suiteStarting( const UnitSuite &rSuite,
        const TestList & /*lTests*/ )
{
    pReport->insert("type", "report");
    pReport->insert("suite", rSuite.getName() );
    pReport->insertArray("tests");
}

void Bu::UnitSuite::ReportJson::testStarting( const TestInfo & )
{
}

void Bu::UnitSuite::ReportJson::updateProgress( int, int )
{
}

void Bu::UnitSuite::ReportJson::testEnded( const TestInfo &rTest )
{
    Bu::Json &rOb = (*pReport)["tests"].appendObject();
    rOb.insert("name", rTest.sName );
    rOb.insert("expected", Bu::String("%1").arg( rTest.eExpect ) );
    rOb.insert("result", "pass");
}

void Bu::UnitSuite::ReportJson::testEnded( const TestInfo &rTest,
        const Bu::UnitSuite::Failed &rFail )
{
    Bu::Json &rOb = (*pReport)["tests"].appendObject();
    rOb.insert("name", rTest.sName );
    rOb.insert("expected", Bu::String("%1").arg( rTest.eExpect ) );
    rOb.insert("result", "fail");
    Bu::Json &rFailOb = rOb.insertObject("fail");
    rFailOb.insert("action", rFail.str );
    rFailOb.insert("file", rFail.sFile );
    rFailOb.insert("line", (double)rFail.nLine );
}

void Bu::UnitSuite::ReportJson::testException( const TestInfo &rTest,
        std::exception &e )
{
    Bu::Json &rOb = (*pReport)["tests"].appendObject();
    rOb.insert("name", rTest.sName );
    rOb.insert("expected", Bu::String("%1").arg( rTest.eExpect ) );
    rOb.insert("result", "fail");
    Bu::Json &rFail = rOb.insertObject("fail");
    rFail.insert("what", e.what() );
}

void Bu::UnitSuite::ReportJson::suiteEnded()
{
    Bu::println("%1").arg( pReport->toString() );
}

