/**
  *  \file u/t_game_interface_vmfile.cpp
  *  \brief Test for game::interface::VmFile
  */

#include "game/interface/vmfile.hpp"

#include "t_game_interface.hpp"
#include "afl/except/fileproblemexception.hpp"
#include "afl/io/filemapping.hpp"
#include "afl/io/nullfilesystem.hpp"
#include "afl/io/stream.hpp"
#include "afl/string/nulltranslator.hpp"
#include "game/game.hpp"
#include "game/test/root.hpp"
#include "game/turn.hpp"
#include "interpreter/bytecodeobject.hpp"
#include "interpreter/process.hpp"

namespace {
    // A file created in unit-testing environment.
    static const uint8_t SAVED_FILE[] = {
        0x43, 0x43, 0x76, 0x6D, 0x31, 0x32, 0x2D, 0x32, 0x34, 0x2D, 0x32, 0x30, 0x32, 0x31, 0x31, 0x32,
        // Sig                  Timestamp
        0x3A, 0x35, 0x30, 0x3A, 0x31, 0x34, 0x1A, 0x64, 0x04, 0x00, 0x46, 0x00, 0x07, 0x00, 0x01, 0x00,
        //                                  Sig   Vers  HeaderBytes TurnNr      PlayerNr   ||Typ:BCO
        0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00,
        //          ObjId 1                 ObjSize 0x58            NumProp 9              |Prop0
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
        //                                  Prop1                                           Prop2
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
        //                                  Prop3                                           Prop4
        0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        //                                  Prop5                                           Prop6
        0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        //                                  Prop7                                           Prop8
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        //                                  Flag        MinArgs     MaxArgs     NumLabels   'ssuspend'
        0x12, 0x0B, 0x53, 0x74, 0x6F, 0x70, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9D, 0x00,
        //          Name: "Stop"          ||Typ:Process             ObjId 0                 ObjSize 0x9D
        0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        //          NumProp 7               Prop0                                           Prop1
        0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x01, 0x00,
        //                                  Prop2                                           Prop3
        0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00,
        //                                  Prop4                                           Prop5
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x00,
        //                                  Prop6                                           Pri   pkDefault
        0x01, 0x00, 0x53, 0x75, 0x73, 0x70, 0x65, 0x6E, 0x64, 0x65, 0x64, 0x20, 0x50, 0x72, 0x6F, 0x63,
        // ctos=1   Name=Suspended Process
        0x65, 0x73, 0x73, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x04,
        //               ||Typ:Frame              ObjId 0                 ObjSize 0x34            Num
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14,
        // Prop 4        |Prop0                                           Prop1
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        //                Prop2                                           Prop3
        0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
        //                BCO-ID                  pc                      contextSP               exSP
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x89, 0x00,
        //                Flags                 || Tag_Global                         Tag_Frame
        0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9A, 0x00, 0x00, 0x00, 0x07,
        //               ||Typ:Process            ObjId 0                 ObjSize 0x9A            Num
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04,
        // Prop7          Prop0                                           Prop1
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x44,
        //                Prop2                                           Prop3
        0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        //                Prop4                                           Prop5
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x00, 0x01, 0x00, 0x46,
        //                Prop6                                           Pri pkDefault ctos     |Name
        0x72, 0x6F, 0x7A, 0x65, 0x6E, 0x20, 0x50, 0x72, 0x6F, 0x63, 0x65, 0x73, 0x73, 0x03, 0x00, 0x00,
        // Frozen Process                                                           ||Typ:Frame
        0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        //    ObjId 0                 ObjSize 0x34            NumProp 4             | Prop0
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        //                            Prop1                                           Prop2
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
        //                            Prop3                                           BCO-ID
        0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        //    pc                      contextSP               exceptionSP             Flags
        0x00, 0x00, 0x8A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x89, 0x00, 0x00, 0x00, 0x00,
        //    Tag_Global                          Tag_Frame
    };

    // A file created by running PCC2
    static const uint8_t REAL_FILE[] = {
        0x43, 0x43, 0x76, 0x6d, 0x30, 0x33, 0x2d, 0x32, 0x34, 0x2d, 0x32, 0x30, 0x32, 0x32, 0x30, 0x33,
        0x3a, 0x30, 0x30, 0x3a, 0x30, 0x32, 0x1a, 0x64, 0x04, 0x00, 0x27, 0x00, 0x02, 0x00, 0x01, 0x00,
        0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
        0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
        0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x12, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x00,
        0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0c, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x32, 0x00, 0x01, 0x00, 0x43, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x3a, 0x20, 0x73,
        0x74, 0x6f, 0x70, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x04,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x89, 0x00,
        0x00, 0x00, 0x00
    };

    struct Environment {
        afl::io::NullFileSystem fs;
        afl::string::NullTranslator tx;
        game::Session session;

        Environment()
            : fs(), tx(), session(tx, fs)
            { }
    };

    void addGame(Environment& env)
    {
        afl::base::Ptr<game::Game> g = new game::Game();
        g->currentTurn().setTimestamp(game::Timestamp(2021, 12, 24, 12, 50, 14));
        g->currentTurn().setTurnNumber(70);
        env.session.setGame(g);
    }

    afl::base::Ref<game::Root> addRoot(Environment& env)
    {
        afl::base::Ref<game::Root> root = game::test::makeRoot(game::HostVersion());
        env.session.setRoot(root.asPtr());
        return root;
    }
}

/** Test loadVM() with a synthetic file. */
void
TestGameInterfaceVmFile::testLoad()
{
    // Environment
    Environment env;
    afl::base::Ref<game::Root> root = addRoot(env);
    root->gameDirectory().openFile("script7.cc", afl::io::FileSystem::Create)
        ->fullWrite(SAVED_FILE);
    addGame(env);

    // Load
    game::interface::loadVM(env.session, 7);

    // Verify
    TS_ASSERT_EQUALS(env.session.processList().getProcessList().size(), 2U);

    interpreter::Process* p1 = env.session.processList().getProcessList()[0];
    TS_ASSERT(p1 != 0);
    TS_ASSERT_EQUALS(p1->getName(), "Suspended Process");

    interpreter::Process* p2 = env.session.processList().getProcessList()[1];
    TS_ASSERT(p2 != 0);
    TS_ASSERT_EQUALS(p2->getName(), "Frozen Process");
}

/** Test loadVM() with a file created by PCC2. */
void
TestGameInterfaceVmFile::testLoad2()
{
    // Environment
    Environment env;
    afl::base::Ref<game::Root> root = addRoot(env);
    root->gameDirectory().openFile("script2.cc", afl::io::FileSystem::Create)
        ->fullWrite(REAL_FILE);
    addGame(env);

    // Load
    game::interface::loadVM(env.session, 2);

    // Verify
    TS_ASSERT_EQUALS(env.session.processList().getProcessList().size(), 1U);

    interpreter::Process* p1 = env.session.processList().getProcessList()[0];
    TS_ASSERT(p1 != 0);
    TS_ASSERT_EQUALS(p1->getName(), "Console: stop");
}

/** Test loadVM() when file does not exist. */
void
TestGameInterfaceVmFile::testLoadMissing()
{
    // Environment
    Environment env;
    addRoot(env);
    addGame(env);

    // Load
    TS_ASSERT_THROWS_NOTHING(game::interface::loadVM(env.session, 2));

    // Verify
    TS_ASSERT_EQUALS(env.session.processList().getProcessList().size(), 0U);
}

/** Test loadVM() with a bad file. */
void
TestGameInterfaceVmFile::testLoadBad()
{
    // Environment
    Environment env;
    afl::base::Ref<game::Root> root = addRoot(env);
    root->gameDirectory().openFile("script2.cc", afl::io::FileSystem::Create);
    addGame(env);

    // Load
    TS_ASSERT_THROWS(game::interface::loadVM(env.session, 2), afl::except::FileProblemException);
}

/** Test loadVM() with a foreign file (wrong player). */
void
TestGameInterfaceVmFile::testLoadForeign()
{
    // Environment
    Environment env;
    afl::base::Ref<game::Root> root = addRoot(env);
    root->gameDirectory().openFile("script3.cc", afl::io::FileSystem::Create)
        ->fullWrite(REAL_FILE);
    addGame(env);

    // Load
    TS_ASSERT_THROWS(game::interface::loadVM(env.session, 3), afl::except::FileProblemException);
}

/** Test saveVM(). */
void
TestGameInterfaceVmFile::testSave()
{
    // Environment
    Environment env;
    afl::base::Ref<game::Root> root = addRoot(env);
    addGame(env);

    // Add some processes
    interpreter::BCORef_t bco = interpreter::BytecodeObject::create(true);
    bco->setSubroutineName("Stop");
    bco->addInstruction(interpreter::Opcode::maSpecial, interpreter::Opcode::miSpecialSuspend, 0);

    // - suspended process (will be saved)
    interpreter::Process& suspendedProcess = env.session.processList().create(env.session.world(), "Suspended Process");
    suspendedProcess.pushFrame(bco, false);
    suspendedProcess.setState(interpreter::Process::Suspended);

    // - frozen process (will be saved)
    interpreter::Process& frozenProcess = env.session.processList().create(env.session.world(), "Frozen Process");
    frozenProcess.pushFrame(bco, false);
    frozenProcess.setState(interpreter::Process::Frozen);
    frozenProcess.setPriority(51);

    // - running process (will not be saved)
    interpreter::Process& runningProcess = env.session.processList().create(env.session.world(), "Running Process");
    runningProcess.pushFrame(bco, false);
    runningProcess.setState(interpreter::Process::Running);

    // - runnable process (will not be saved)
    interpreter::Process& runnableProcess = env.session.processList().create(env.session.world(), "Runnable Process");
    runnableProcess.pushFrame(bco, false);
    runnableProcess.setState(interpreter::Process::Runnable);

    // - terminated process (will not be saved)
    interpreter::Process& terminatedProcess = env.session.processList().create(env.session.world(), "Terminated Process");
    terminatedProcess.pushFrame(bco, false);
    terminatedProcess.setState(interpreter::Process::Terminated);

    // - failed process (will not be saved)
    interpreter::Process& failedProcess = env.session.processList().create(env.session.world(), "Failed Process");
    failedProcess.pushFrame(bco, false);
    failedProcess.setState(interpreter::Process::Failed);

    // Save
    game::interface::saveVM(env.session, 7);

    // Verify
    afl::base::Ref<afl::io::FileMapping> m = root->gameDirectory().openFile("script7.cc", afl::io::FileSystem::OpenRead)->createVirtualMapping();
    TS_ASSERT_EQUALS(m->get().size(), sizeof(SAVED_FILE));
    TS_ASSERT_SAME_DATA(m->get().at(0), SAVED_FILE, sizeof(SAVED_FILE));
}

/** Test saveVM() with no processes.
    VM file will be deleted. */
void
TestGameInterfaceVmFile::testSaveEmpty()
{
    // Environment
    Environment env;
    afl::base::Ref<game::Root> root = addRoot(env);
    root->gameDirectory().openFile("script7.cc", afl::io::FileSystem::Create);
    addGame(env);

    // Save
    game::interface::saveVM(env.session, 7);

    // Verify
    TS_ASSERT(root->gameDirectory().openFileNT("script7.cc", afl::io::FileSystem::OpenRead).get() == 0);
}

/** Test saveVM() with missing preconditions. */
void
TestGameInterfaceVmFile::testSaveNull()
{
    // Environment
    Environment env;
    afl::base::Ref<game::Root> root = addRoot(env);

    // Save: no-op, because no game
    TS_ASSERT_THROWS_NOTHING(game::interface::saveVM(env.session, 7));

    // Verify
    TS_ASSERT(root->gameDirectory().openFileNT("script7.cc", afl::io::FileSystem::OpenRead).get() == 0);
}

