/**
  *  \file u/t_game_v3_turnfile.cpp
  *  \brief Test for game::v3::TurnFile
  */

#include "game/v3/turnfile.hpp"

#include "t_game_v3.hpp"
#include "afl/charset/codepage.hpp"
#include "afl/charset/codepagecharset.hpp"
#include "afl/except/fileformatexception.hpp"
#include "afl/io/constmemorystream.hpp"
#include "afl/io/internalstream.hpp"
#include "afl/string/nulltranslator.hpp"
#include "game/timestamp.hpp"
#include "game/v3/registrationkey.hpp"
#include "afl/base/countof.hpp"

using afl::base::ConstBytes_t;
using afl::charset::Charset;
using afl::charset::CodepageCharset;
using afl::except::FileFormatException;
using afl::io::ConstMemoryStream;
using afl::io::InternalStream;
using afl::string::NullTranslator;
using game::Timestamp;
using game::v3::RegistrationKey;
using game::v3::TurnFile;

namespace {
    /* A version 3.5 turn file, made from Tim's example RST using PCC2 */
    const uint8_t TURN35[] = {
        0x0b, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x30, 0x31, 0x2d, 0x31, 0x32, 0x2d, 0x31, 0x39, 0x39, 0x36,
        0x30, 0x32, 0x3a, 0x30, 0x30, 0x3a, 0x32, 0x37, 0x00, 0x00, 0x96, 0x03, 0x00, 0x46, 0x00, 0x00,
        0x00, 0x58, 0x00, 0x00, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x00,
        0x00, 0xa0, 0x00, 0x00, 0x00, 0xb2, 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0xd6, 0x00, 0x00,
        0x00, 0xe8, 0x00, 0x00, 0x00, 0x35, 0x00, 0x8b, 0x00, 0x07, 0x00, 0x09, 0x00, 0x06, 0x00, 0x04,
        0x00, 0x09, 0x00, 0x02, 0x00, 0x00, 0x00, 0x35, 0x00, 0xbd, 0x00, 0x07, 0x00, 0x09, 0x00, 0x0a,
        0x00, 0x04, 0x00, 0x09, 0x00, 0x02, 0x00, 0x00, 0x00, 0x35, 0x00, 0xd1, 0x00, 0x13, 0x00, 0x09,
        0x00, 0x0a, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x35, 0x00, 0xe3, 0x00, 0x13,
        0x00, 0x09, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x35, 0x00, 0x0d,
        0x01, 0x07, 0x00, 0x09, 0x00, 0x09, 0x00, 0x04, 0x00, 0x09, 0x00, 0x02, 0x00, 0x00, 0x00, 0x35,
        0x00, 0x44, 0x01, 0x13, 0x00, 0x09, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x35, 0x00, 0x9e, 0x01, 0x13, 0x00, 0x09, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x35, 0x00, 0xb8, 0x01, 0x13, 0x00, 0x09, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x35, 0x00, 0xcc, 0x01, 0x07, 0x00, 0x09, 0x00, 0x0a, 0x00, 0x04,
        0x00, 0x09, 0x00, 0x02, 0x00, 0x00, 0x00, 0x35, 0x00, 0xd4, 0x01, 0x07, 0x00, 0x09, 0x00, 0x0a,
        0x00, 0x04, 0x00, 0x09, 0x00, 0x02, 0x00, 0x00, 0x00, 0x56, 0x45, 0x52, 0x33, 0x2e, 0x35, 0x30,
        0x31, 0x62, 0xbd, 0xf9, 0x5e, 0x65, 0xa0, 0x56, 0x6d, 0xf8, 0x3c, 0x32, 0x07, 0xf0, 0x6b, 0xdc,
        0x97, 0x2d, 0x1d, 0x9d, 0x0e, 0x91, 0xbf, 0x6b, 0x09, 0xb6, 0x48, 0x78, 0xeb, 0x0c, 0x7a, 0x82,
        0x93, 0x98, 0xae, 0x7b, 0x73, 0x27, 0xa0, 0x07, 0xbd, 0xf9, 0x48, 0x69, 0xee, 0x2e, 0xe2, 0xd7,
        0x0a, 0x7b, 0xd3, 0x3f, 0x19, 0x99, 0x69, 0x5a, 0xa2, 0xb3, 0xb8, 0xc3, 0xe1, 0x01, 0xe3, 0x43,
        0xe0, 0x78, 0xac, 0xde, 0x96, 0x02, 0x93, 0xb7, 0xe0, 0x3e, 0xf9, 0x80, 0x91, 0x02, 0xc7, 0xd5,
        0xaa, 0xc0, 0xd5, 0x2e, 0x95, 0x84, 0x73, 0x90, 0x2a, 0x8f, 0x16, 0x8c, 0xed, 0xb8, 0x32, 0xa3,
        0x97, 0xc0, 0x1e, 0xd9, 0xa0, 0xb1, 0x22, 0xe7, 0xf5, 0x8a, 0xe0, 0xf5, 0x0e, 0x43, 0x6c, 0x69,
        0x65, 0x6e, 0x74, 0x3a, 0x20, 0x50, 0x6c, 0x61, 0x6e, 0x65, 0x74, 0x73, 0x20, 0x43, 0x6f, 0x6d,
        0x6d, 0x61, 0x6e, 0x64, 0x20, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x20, 0x49, 0x49, 0x20, 0x28,
        0x76, 0x32, 0x2e, 0x30, 0x2e, 0x31, 0x32, 0x29, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x68,
        0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x70, 0x68, 0x6f, 0x73, 0x74, 0x2e, 0x64, 0x65, 0x2f, 0x7e,
        0x73, 0x74, 0x65, 0x66, 0x61, 0x6e, 0x2f, 0x70, 0x63, 0x63, 0x32, 0x2e, 0x68, 0x74, 0x6d, 0x6c,
        0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
        0x20, 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, 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, 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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x77, 0x00, 0x00, 0x50, 0x43, 0x43, 0x32, 0x5e, 0x04, 0x00,
        0x00, 0x36, 0x07, 0x00, 0x00, 0xe7, 0x09, 0x00, 0x00, 0x80, 0x06, 0x00, 0x00, 0x50, 0x14, 0x00,
        0x00, 0xe8, 0x20, 0x00, 0x00, 0x7b, 0x22, 0x00, 0x00, 0xb0, 0x2c, 0x00, 0x00, 0x29, 0x2e, 0x00,
        0x00, 0xe8, 0x3a, 0x00, 0x00, 0x3d, 0x40, 0x00, 0x00, 0x80, 0x13, 0x00, 0x00, 0xeb, 0x4b, 0x00,
        0x00, 0xf0, 0x49, 0x00, 0x00, 0xe3, 0x49, 0x00, 0x00, 0xa0, 0x5c, 0x00, 0x00, 0x31, 0x57, 0x00,
        0x00, 0xc6, 0x6c, 0x00, 0x00, 0x97, 0x5d, 0x00, 0x00, 0xc8, 0x73, 0x00, 0x00, 0xb5, 0x6b, 0x00,
        0x00, 0xc0, 0x23, 0x00, 0x00, 0x60, 0x25, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0xa0, 0x28, 0x00,
        0x00, 0x5e, 0x04, 0x00, 0x00, 0x42, 0x0a, 0x00, 0x00, 0x5e, 0x11, 0x00, 0x00, 0x5c, 0x17, 0x00,
        0x00, 0xa9, 0x1a, 0x00, 0x00, 0xd2, 0x21, 0x00, 0x00, 0x1a, 0x27, 0x00, 0x00, 0x00, 0x0d, 0x00,
        0x00, 0x4f, 0x17, 0x00, 0x00, 0x5c, 0x17, 0x00, 0x00, 0xd0, 0x1a, 0x00, 0x00, 0x40, 0x1d, 0x00,
        0x00, 0x20, 0x15, 0x00, 0x00, 0xc0, 0x16, 0x00, 0x00, 0x60, 0x18, 0x00, 0x00, 0x00, 0x1a, 0x00,
        0x00, 0xa0, 0x1b, 0x00, 0x00, 0x40, 0x1d, 0x00, 0x00, 0xe0, 0x1e, 0x00, 0x00, 0x80, 0x20, 0x00,
        0x00, 0x20, 0x22, 0x00, 0x00, 0xc0, 0x23, 0x00, 0x00, 0x60, 0x25, 0x00, 0x00, 0x00, 0x27, 0x00,
        0x00, 0xa0, 0x28, 0x00, 0x00, 0xfb, 0xd5, 0x07, 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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x01, 0x77, 0x00, 0x00
    };

    /* A version 3.0 turn file, made from Tim's example RST using PCC1 */
    const uint8_t TURN30[] = {
        0x0b, 0x00, 0x06, 0x00, 0x00, 0x00, 0x30, 0x31, 0x2d, 0x31, 0x32, 0x2d, 0x31, 0x39, 0x39, 0x36,
        0x30, 0x32, 0x3a, 0x30, 0x30, 0x3a, 0x32, 0x37, 0x00, 0x00, 0x96, 0x03, 0x00, 0x36, 0x00, 0x00,
        0x00, 0x3c, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x00,
        0x00, 0x54, 0x00, 0x00, 0x00, 0x02, 0x00, 0x5b, 0x00, 0x00, 0x00, 0x02, 0x00, 0x71, 0x00, 0x09,
        0x00, 0x02, 0x00, 0xc5, 0x00, 0x09, 0x00, 0x02, 0x00, 0xcd, 0x00, 0x09, 0x00, 0x02, 0x00, 0x5a,
        0x01, 0x00, 0x00, 0x02, 0x00, 0xad, 0x01, 0x09, 0x00, 0x44, 0x14, 0x00, 0x00, 0x50, 0x43, 0x43,
        0x21, 0x5e, 0x04, 0x00, 0x00, 0x36, 0x07, 0x00, 0x00, 0xe7, 0x09, 0x00, 0x00, 0x80, 0x06, 0x00,
        0x00, 0x50, 0x14, 0x00, 0x00, 0xe8, 0x20, 0x00, 0x00, 0x7b, 0x22, 0x00, 0x00, 0xb0, 0x2c, 0x00,
        0x00, 0x29, 0x2e, 0x00, 0x00, 0xe8, 0x3a, 0x00, 0x00, 0x3d, 0x40, 0x00, 0x00, 0x80, 0x13, 0x00,
        0x00, 0xeb, 0x4b, 0x00, 0x00, 0xf0, 0x49, 0x00, 0x00, 0xe3, 0x49, 0x00, 0x00, 0xa0, 0x5c, 0x00,
        0x00, 0x31, 0x57, 0x00, 0x00, 0xc6, 0x6c, 0x00, 0x00, 0x97, 0x5d, 0x00, 0x00, 0xc8, 0x73, 0x00,
        0x00, 0xb5, 0x6b, 0x00, 0x00, 0xc0, 0x23, 0x00, 0x00, 0x60, 0x25, 0x00, 0x00, 0x00, 0x27, 0x00,
        0x00, 0xa0, 0x28, 0x00, 0x00, 0x5e, 0x04, 0x00, 0x00, 0x42, 0x0a, 0x00, 0x00, 0x5e, 0x11, 0x00,
        0x00, 0x5c, 0x17, 0x00, 0x00, 0xa9, 0x1a, 0x00, 0x00, 0xd2, 0x21, 0x00, 0x00, 0x1a, 0x27, 0x00,
        0x00, 0x00, 0x0d, 0x00, 0x00, 0x4f, 0x17, 0x00, 0x00, 0x5c, 0x17, 0x00, 0x00, 0xd0, 0x1a, 0x00,
        0x00, 0x40, 0x1d, 0x00, 0x00, 0x20, 0x15, 0x00, 0x00, 0xc0, 0x16, 0x00, 0x00, 0x60, 0x18, 0x00,
        0x00, 0x00, 0x1a, 0x00, 0x00, 0xa0, 0x1b, 0x00, 0x00, 0x40, 0x1d, 0x00, 0x00, 0xe0, 0x1e, 0x00,
        0x00, 0x80, 0x20, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0xc0, 0x23, 0x00, 0x00, 0x60, 0x25, 0x00,
        0x00, 0x00, 0x27, 0x00, 0x00, 0xa0, 0x28, 0x00, 0x00, 0xfb, 0xd5, 0x07, 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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x14, 0x00, 0x00
    };

    /* A version 3.0 turn file with attachments */
    const uint8_t TURN30_ATTACHMENT[] = {
        0x4e, 0x43, 0x43, 0x31, 0x37, 0x30, 0x31, 0x41, 0x44, 0x39, 0xdb, 0x00, 0x00, 0x00, 0x59, 0x01,
        0x00, 0x00, 0x34, 0x02, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x70, 0x63, 0x63, 0x32, 0x2e, 0x69,
        0x6e, 0x69, 0x20, 0x20, 0x20, 0x20, 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, 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, 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, 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, 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, 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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x06, 0x00, 0x00, 0x00,
        0x30, 0x31, 0x2d, 0x31, 0x32, 0x2d, 0x31, 0x39, 0x39, 0x36, 0x30, 0x32, 0x3a, 0x30, 0x30, 0x3a,
        0x32, 0x37, 0x00, 0x00, 0x96, 0x03, 0x00, 0x36, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x42,
        0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x02,
        0x00, 0x5b, 0x00, 0x00, 0x00, 0x02, 0x00, 0x71, 0x00, 0x09, 0x00, 0x02, 0x00, 0xc5, 0x00, 0x09,
        0x00, 0x02, 0x00, 0xcd, 0x00, 0x09, 0x00, 0x02, 0x00, 0x5a, 0x01, 0x00, 0x00, 0x02, 0x00, 0xad,
        0x01, 0x09, 0x00, 0x44, 0x14, 0x00, 0x00, 0x50, 0x43, 0x43, 0x21, 0x5e, 0x04, 0x00, 0x00, 0x36,
        0x07, 0x00, 0x00, 0xe7, 0x09, 0x00, 0x00, 0x80, 0x06, 0x00, 0x00, 0x50, 0x14, 0x00, 0x00, 0xe8,
        0x20, 0x00, 0x00, 0x7b, 0x22, 0x00, 0x00, 0xb0, 0x2c, 0x00, 0x00, 0x29, 0x2e, 0x00, 0x00, 0xe8,
        0x3a, 0x00, 0x00, 0x3d, 0x40, 0x00, 0x00, 0x80, 0x13, 0x00, 0x00, 0xeb, 0x4b, 0x00, 0x00, 0xf0,
        0x49, 0x00, 0x00, 0xe3, 0x49, 0x00, 0x00, 0xa0, 0x5c, 0x00, 0x00, 0x31, 0x57, 0x00, 0x00, 0xc6,
        0x6c, 0x00, 0x00, 0x97, 0x5d, 0x00, 0x00, 0xc8, 0x73, 0x00, 0x00, 0xb5, 0x6b, 0x00, 0x00, 0xc0,
        0x23, 0x00, 0x00, 0x60, 0x25, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0xa0, 0x28, 0x00, 0x00, 0x5e,
        0x04, 0x00, 0x00, 0x42, 0x0a, 0x00, 0x00, 0x5e, 0x11, 0x00, 0x00, 0x5c, 0x17, 0x00, 0x00, 0xa9,
        0x1a, 0x00, 0x00, 0xd2, 0x21, 0x00, 0x00, 0x1a, 0x27, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x4f,
        0x17, 0x00, 0x00, 0x5c, 0x17, 0x00, 0x00, 0xd0, 0x1a, 0x00, 0x00, 0x40, 0x1d, 0x00, 0x00, 0x20,
        0x15, 0x00, 0x00, 0xc0, 0x16, 0x00, 0x00, 0x60, 0x18, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0xa0,
        0x1b, 0x00, 0x00, 0x40, 0x1d, 0x00, 0x00, 0xe0, 0x1e, 0x00, 0x00, 0x80, 0x20, 0x00, 0x00, 0x20,
        0x22, 0x00, 0x00, 0xc0, 0x23, 0x00, 0x00, 0x60, 0x25, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0xa0,
        0x28, 0x00, 0x00, 0xfb, 0xd5, 0x07, 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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44,
        0x14, 0x00, 0x00, 0x43, 0x68, 0x61, 0x72, 0x74, 0x2e, 0x47, 0x65, 0x6f, 0x2e, 0x4d, 0x6f, 0x64,
        0x65, 0x20, 0x3d, 0x20, 0x66, 0x6c, 0x61, 0x74, 0x0a, 0x43, 0x68, 0x61, 0x72, 0x74, 0x2e, 0x47,
        0x65, 0x6f, 0x2e, 0x53, 0x69, 0x7a, 0x65, 0x20, 0x3d, 0x20, 0x31, 0x36, 0x30, 0x30, 0x2c, 0x31,
        0x36, 0x30, 0x30, 0x0a
    };

    ConstBytes_t stripLastByte(ConstBytes_t m)
    {
        return m.trim(m.size()-1);
    }

    ConstBytes_t trim(ConstBytes_t m, size_t n)
    {
        return m.trim(n);
    }
}

/** Test initialisation from scratch.
    A: execute "create new turn sequence".
    E: verify correct result produced */
void
TestGameV3TurnFile::testInit()
{
    CodepageCharset cs(afl::charset::g_codepage437);
    Timestamp ts(2000, 12, 24, 18, 30, 0);

    // - construct using player, charset, timestamp
    TurnFile testee(cs, 7, ts);
    TS_ASSERT_EQUALS(testee.getPlayer(), 7);
    TS_ASSERT_EQUALS(testee.getTimestamp().getTimestampAsString(), "12-24-200018:30:00");
    TS_ASSERT_EQUALS(testee.getNumCommands(), 0U);
    TS_ASSERT_EQUALS(testee.getFeatures(), TurnFile::FeatureSet_t(TurnFile::WinplanFeature));
    TS_ASSERT_EQUALS(&testee.charset(), &cs);

    // - first set the format (setFeatures, setVersion)
    testee.setFeatures(TurnFile::FeatureSet_t());

    // - then set the registration key (setRegistrationKey)
    RegistrationKey key(std::auto_ptr<Charset>(cs.clone()));
    key.initUnregistered();
    testee.setRegistrationKey(key, 42);

    // - addCommand() can be called anywhere inbetween
    uint8_t arg[] = { 'a','b','c' };
    testee.addCommand(game::v3::tcm_ShipChangeFc, 314, arg);

    // - rebuild the turn (update)
    testee.update();
    TS_ASSERT_EQUALS(testee.getNumCommands(), 1U);

    // - write it out.
    InternalStream out;
    testee.write(out);

    // Verify
    // - size is 29 (header) + 4 (command pos) + 7 (command) + 256 (DOS trailer) = 296
    TS_ASSERT_EQUALS(out.getSize(), 296U);

    // Verify some content
    ConstBytes_t mem = out.getContent();
    //  +0     WORD    Player Id
    TS_ASSERT_EQUALS(*mem.eat(), 7);
    TS_ASSERT_EQUALS(*mem.eat(), 0);
    //  +2     DWORD   Number of commands. THost accepts no more than 5000.
    TS_ASSERT_EQUALS(*mem.eat(), 1);
    TS_ASSERT_EQUALS(*mem.eat(), 0);
    TS_ASSERT_EQUALS(*mem.eat(), 0);
    TS_ASSERT_EQUALS(*mem.eat(), 0);
    //  +6  10 BYTEs   Date of last turn: mm-dd-yyyy
    // +16   8 BYTEs   Time of last turn: hh:mm:ss
    TS_ASSERT_EQUALS(afl::string::fromBytes(mem.split(18)), "12-24-200018:30:00");
    // +24     WORD    unused
    mem.split(2);
    // +26     WORD    Timestamp Checksum
    TS_ASSERT_EQUALS(*mem.eat(), 133);
    TS_ASSERT_EQUALS(*mem.eat(), 3);
    // --- only, if commands exist ---
    // +28     BYTE    unused
    mem.split(1);
    // +29   n DWORDs  File positions of the commands, each increased by 1
    TS_ASSERT_EQUALS(*mem.eat(), 34);
    TS_ASSERT_EQUALS(*mem.eat(), 0);
    TS_ASSERT_EQUALS(*mem.eat(), 0);
    TS_ASSERT_EQUALS(*mem.eat(), 0);
    // Command:
    //  +0     WORD    Command Code
    //  +2   n BYTEs   Data
    TS_ASSERT_EQUALS(*mem.eat(), 1);
    TS_ASSERT_EQUALS(*mem.eat(), 0);
    TS_ASSERT_EQUALS(*mem.eat(), 314 % 256);
    TS_ASSERT_EQUALS(*mem.eat(), 314 / 256);
    TS_ASSERT_EQUALS(*mem.eat(), 'a');
    TS_ASSERT_EQUALS(*mem.eat(), 'b');
    TS_ASSERT_EQUALS(*mem.eat(), 'c');
}

/** Test parsing a "3.0" turn file.
    A: parse a turn file
    E: verify properties of turn file */
void
TestGameV3TurnFile::testParse30()
{
    // Setup
    CodepageCharset cs(afl::charset::g_codepage437);
    NullTranslator tx;
    ConstMemoryStream ms(TURN30);
    TurnFile testee(cs, tx, ms);

    // Parse
    TS_ASSERT_EQUALS(testee.getTimestamp().getTimestampAsString(), "01-12-199602:00:27");
    TS_ASSERT_EQUALS(testee.getNumCommands(), 6U);
    TS_ASSERT_EQUALS(testee.getPlayer(), 11);
    TS_ASSERT_EQUALS(testee.getTurnHeader().timeChecksum, 918);
    TS_ASSERT_EQUALS(testee.getFeatures(), TurnFile::FeatureSet_t());
    TS_ASSERT_EQUALS(testee.computeTurnChecksum(), 0x1444U);

    // Windows trailer and Taccom header is empty
    TS_ASSERT_EQUALS(afl::base::fromObject(testee.getWindowsTrailer()).findNot(0),
                     sizeof(game::v3::structures::TurnWindowsTrailer));
    TS_ASSERT_EQUALS(afl::base::fromObject(testee.getTaccomHeader()).findNot(0),
                     sizeof(game::v3::structures::TaccomTurnHeader));

    // Verify a command
    TurnFile::CommandCode_t code;
    TS_ASSERT(testee.getCommandCode(2, code));
    TS_ASSERT_EQUALS(code, uint32_t(game::v3::tcm_ShipChangeSpeed));

    int length;
    TS_ASSERT(testee.getCommandLength(2, length));
    TS_ASSERT_EQUALS(length, 2);

    int id;
    TS_ASSERT(testee.getCommandId(2, id));
    TS_ASSERT_EQUALS(id, 197);

    const char* name = testee.getCommandName(2);
    TS_ASSERT(name != 0);
    TS_ASSERT_EQUALS(String_t(name), "ShipChangeSpeed");

    ConstBytes_t data = testee.getCommandData(2);
    TS_ASSERT(data.size() >= 2);
    TS_ASSERT_EQUALS(*data.eat(), 9);
    TS_ASSERT_EQUALS(*data.eat(), 0);
}

/** Test parsing a "3.0" turn file with attachments.
    A: parse a turn file
    E: verify properties of turn file and attachments */
void
TestGameV3TurnFile::testParse30Attachment()
{
    // Setup
    CodepageCharset cs(afl::charset::g_codepage437);
    NullTranslator tx;
    ConstMemoryStream ms(TURN30_ATTACHMENT);
    TurnFile testee(cs, tx, ms);

    // Parse
    TS_ASSERT_EQUALS(testee.getTimestamp().getTimestampAsString(), "01-12-199602:00:27");
    TS_ASSERT_EQUALS(testee.getNumCommands(), 6U);
    TS_ASSERT_EQUALS(testee.getPlayer(), 11);
    TS_ASSERT_EQUALS(testee.getTurnHeader().timeChecksum, 918);
    TS_ASSERT_EQUALS(testee.getFeatures(), TurnFile::FeatureSet_t(TurnFile::TaccomFeature));

    // Verify a command (abridged)
    int id;
    TS_ASSERT(testee.getCommandId(2, id));
    TS_ASSERT_EQUALS(id, 197);

    ConstBytes_t data = testee.getCommandData(2);
    TS_ASSERT(data.size() >= 2);
    TS_ASSERT_EQUALS(*data.eat(), 9);
    TS_ASSERT_EQUALS(*data.eat(), 0);

    // Verify attachment
    TS_ASSERT_EQUALS(testee.getNumFiles(), 1U);
    TS_ASSERT_EQUALS(testee.getTaccomTurnPlace(), 0U);
    TS_ASSERT_EQUALS(cs.decode(testee.getTaccomHeader().attachments[0].name), "pcc2.ini");

    // Regenerate
    InternalStream out;
    testee.update();
    testee.write(out);

    // Data must be substantially identical. Since we mess with the 'id' field, it will not be entirely identical, though.
    TS_ASSERT_EQUALS(out.getSize(), sizeof(TURN30_ATTACHMENT));
    TS_ASSERT_SAME_DATA(out.getContent().unsafeData(), TURN30_ATTACHMENT, 300);
}

/** Test parsing a "3.0" turn file with attachments, header only.
    A: parse a turn file
    E: verify properties of turn file; header-only parsing will ignore attachments */
void
TestGameV3TurnFile::testParse30AttachmentHeader()
{
    // Setup
    CodepageCharset cs(afl::charset::g_codepage437);
    NullTranslator tx;
    ConstMemoryStream ms(TURN30_ATTACHMENT);
    TurnFile testee(cs, tx, ms, false);

    // Parse
    TS_ASSERT_EQUALS(testee.getTimestamp().getTimestampAsString(), "01-12-199602:00:27");
    TS_ASSERT_EQUALS(testee.getNumCommands(), 0U);
    TS_ASSERT_EQUALS(testee.getPlayer(), 11);
    TS_ASSERT_EQUALS(testee.getTurnHeader().timeChecksum, 918);
    TS_ASSERT_EQUALS(testee.getFeatures(), TurnFile::FeatureSet_t());
    TS_ASSERT_EQUALS(testee.getNumFiles(), 0U);
}

/** Test parsing a "3.5" turn file.
    A: parse a turn file
    E: verify properties of turn file */
void
TestGameV3TurnFile::testParse35()
{
    // Setup
    CodepageCharset cs(afl::charset::g_codepage437);
    NullTranslator tx;
    ConstMemoryStream ms(TURN35);
    TurnFile testee(cs, tx, ms);

    // Parse
    TS_ASSERT_EQUALS(testee.getTimestamp().getTimestampAsString(), "01-12-199602:00:27");
    TS_ASSERT_EQUALS(testee.getNumCommands(), 10U);
    TS_ASSERT_EQUALS(testee.getPlayer(), 11);
    TS_ASSERT_EQUALS(testee.getTurnHeader().timeChecksum, 918);
    TS_ASSERT_EQUALS(testee.getFeatures(), TurnFile::FeatureSet_t(TurnFile::WinplanFeature));
    TS_ASSERT_EQUALS(testee.getVersion(), 1);
    TS_ASSERT_EQUALS(testee.tryGetTurnNr(), 117);
    TS_ASSERT_EQUALS(testee.computeTurnChecksum(), 0x7701U);

    // Verify a command
    TurnFile::CommandCode_t code;
    TS_ASSERT(testee.getCommandCode(5, code));
    TS_ASSERT_EQUALS(code, uint32_t(game::v3::tcm_BaseBuildShip));

    int length;
    TS_ASSERT(testee.getCommandLength(5, length));
    TS_ASSERT_EQUALS(length, 14);

    int id;
    TS_ASSERT(testee.getCommandId(5, id));
    TS_ASSERT_EQUALS(id, 324);

    const char* name = testee.getCommandName(5);
    TS_ASSERT(name != 0);
    TS_ASSERT_EQUALS(String_t(name), "BaseBuildShip");

    ConstBytes_t data = testee.getCommandData(5);
    TS_ASSERT(data.size() >= 14);
    TS_ASSERT_EQUALS(*data.eat(), 19);   // hull type
    TS_ASSERT_EQUALS(*data.eat(), 0);
    TS_ASSERT_EQUALS(*data.eat(), 9);    // engine type
    TS_ASSERT_EQUALS(*data.eat(), 0);
    TS_ASSERT_EQUALS(*data.eat(), 10);   // beam type
    TS_ASSERT_EQUALS(*data.eat(), 0);
}

/** Test parsing a "3.5" turn file, header-only mode.
    A: parse a turn file
    E: verify properties of turn file */
void
TestGameV3TurnFile::testParse35Header()
{
    // Setup
    CodepageCharset cs(afl::charset::g_codepage437);
    NullTranslator tx;
    ConstMemoryStream ms(TURN35);
    TurnFile testee(cs, tx, ms, false);

    // Parse
    TS_ASSERT_EQUALS(testee.getTimestamp().getTimestampAsString(), "01-12-199602:00:27");
    TS_ASSERT_EQUALS(testee.getNumCommands(), 0U);
    TS_ASSERT_EQUALS(testee.getPlayer(), 11);
    TS_ASSERT_EQUALS(testee.getTurnHeader().timeChecksum, 918);
    TS_ASSERT_EQUALS(testee.getFeatures(), TurnFile::FeatureSet_t(TurnFile::WinplanFeature));
    TS_ASSERT_EQUALS(testee.getVersion(), 1);

    // Command access fails
    TurnFile::CommandCode_t code;
    TS_ASSERT(!testee.getCommandCode(5, code));

    int length;
    TS_ASSERT(!testee.getCommandLength(5, length));

    int id;
    TS_ASSERT(!testee.getCommandId(5, id));

    const char* name = testee.getCommandName(5);
    TS_ASSERT(name == 0);

    ConstBytes_t data = testee.getCommandData(5);
    TS_ASSERT(data.empty());
}

/** Test parsing damaged files.
    A: parse truncated files
    E: parsing fails with FileFormatException */
void
TestGameV3TurnFile::testParseDamaged()
{
    CodepageCharset cs(afl::charset::g_codepage437);
    NullTranslator tx;

    {
        // Minimum size is 29 + 6*4 + 256 = 309, but otherwise we don't validate that trailer and commands don't overlap
        ConstMemoryStream ms(trim(TURN30, 308));
        TS_ASSERT_THROWS(TurnFile(cs, tx, ms), FileFormatException);
    }
    {
        // Failure to read (turn) headers
        ConstMemoryStream ms(trim(TURN30, 10));
        TS_ASSERT_THROWS(TurnFile(cs, tx, ms), FileFormatException);
    }
    {
        // With attachments, we can detect a missing last byte
        ConstMemoryStream ms(stripLastByte(TURN30_ATTACHMENT));
        TS_ASSERT_THROWS(TurnFile(cs, tx, ms), FileFormatException);
    }
    {
        // Failure to read (attachment) headers
        ConstMemoryStream ms(trim(TURN30_ATTACHMENT, 10));
        TS_ASSERT_THROWS(TurnFile(cs, tx, ms), FileFormatException);
    }
}

/** Test deleteCommand().
    A: load turn file, delete a command, write out again
    E: new turn file must be shorter */
void
TestGameV3TurnFile::testDeleteCommand()
{
    // Load a turn file
    CodepageCharset cs(afl::charset::g_codepage437);
    NullTranslator tx;
    ConstMemoryStream ms(TURN30);
    TurnFile testee(cs, tx, ms);
    TS_ASSERT_EQUALS(testee.getNumCommands(), 6U);

    // Remove a command
    testee.deleteCommand(3);
    testee.update();

    // Write out again
    InternalStream out;
    testee.write(out);

    // The new file must be 10 bytes shorter (4 bytes index + 6 bytes command)
    TS_ASSERT_EQUALS(out.getSize(), ms.getSize() - 10);
    TS_ASSERT_EQUALS(testee.getNumCommands(), 5U);
}

/** Test sendTHostAllies().
    A: create new turn file. Use sendTHostAllies().
    E: verify correct commands generated */
void
TestGameV3TurnFile::testSendTHostAllies()
{
    // Empty turn file
    CodepageCharset cs(afl::charset::g_codepage437);
    Timestamp ts(2000, 12, 24, 18, 30, 0);
    TurnFile testee(cs, 7, ts);

    // Send commands
    testee.sendTHostAllies("ff1ee5", 42, "lfm");
    testee.update();

    // Verify
    TS_ASSERT_EQUALS(testee.getNumCommands(), 3U);
    for (int i = 0; i < 3; ++i) {
        TurnFile::CommandCode_t code;
        TS_ASSERT(testee.getCommandCode(i, code));
        TS_ASSERT_EQUALS(code, uint32_t(game::v3::tcm_ShipChangeFc));

        int id;
        TS_ASSERT(testee.getCommandId(i, id));
        TS_ASSERT_EQUALS(id, 42);
    }

    ConstBytes_t b = testee.getCommandData(0);
    TS_ASSERT_EQUALS(*b.eat(), 'f');
    TS_ASSERT_EQUALS(*b.eat(), 'f');
    TS_ASSERT_EQUALS(*b.eat(), '1');

    b = testee.getCommandData(1);
    TS_ASSERT_EQUALS(*b.eat(), 'e');
    TS_ASSERT_EQUALS(*b.eat(), 'e');
    TS_ASSERT_EQUALS(*b.eat(), '5');

    b = testee.getCommandData(2);
    TS_ASSERT_EQUALS(*b.eat(), 'l');
    TS_ASSERT_EQUALS(*b.eat(), 'f');
    TS_ASSERT_EQUALS(*b.eat(), 'm');
}

/** Test sendMessageData().
    A: create new turn file. Use sendMessageData().
    E: verify correct command generated */
void
TestGameV3TurnFile::testSendMessageData()
{
    // Empty turn file
    CodepageCharset cs(afl::charset::g_codepage437);
    Timestamp ts(2000, 12, 24, 18, 30, 0);
    TurnFile testee(cs, 7, ts);

    // Send
    static const uint8_t MSG[] = {'z','x','a','b'};
    testee.sendMessageData(7, 3, MSG);

    // Verify
    TS_ASSERT_EQUALS(testee.getNumCommands(), 1U);

    TurnFile::CommandCode_t code;
    TS_ASSERT(testee.getCommandCode(0, code));
    TS_ASSERT_EQUALS(code, uint32_t(game::v3::tcm_SendMessage));

    int id; // length of message
    TS_ASSERT(testee.getCommandId(0, id));
    TS_ASSERT_EQUALS(id, 4);

    ConstBytes_t b = testee.getCommandData(0);
    TS_ASSERT(b.size() >= 8);
    // - from
    TS_ASSERT_EQUALS(*b.eat(), 7);
    TS_ASSERT_EQUALS(*b.eat(), 0);
    // - to
    TS_ASSERT_EQUALS(*b.eat(), 3);
    TS_ASSERT_EQUALS(*b.eat(), 0);
    // - content
    TS_ASSERT_EQUALS(*b.eat(), 'z');
    TS_ASSERT_EQUALS(*b.eat(), 'x');
    TS_ASSERT_EQUALS(*b.eat(), 'a');
    TS_ASSERT_EQUALS(*b.eat(), 'b');
}

/** Test makeShipCommands().
    A: create new turn file. Use makeShipCommands().
    E: verify correct commands generated */
void
TestGameV3TurnFile::testMakeShipCommands()
{
    // Empty turn file
    CodepageCharset cs(afl::charset::g_codepage437);
    Timestamp ts(2000, 12, 24, 18, 30, 0);
    TurnFile testee(cs, 7, ts);

    // Define a ship
    game::v3::structures::Ship oldShip;
    game::v3::structures::Ship newShip;
    afl::base::fromObject(oldShip).fill(0);
    afl::base::fromObject(newShip).fill(0);
    oldShip.shipId = 33;
    newShip.shipId = 33;
    oldShip.owner = 7;
    newShip.owner = 7;
    oldShip.mission = 4;
    newShip.mission = 9;
    oldShip.primaryEnemy = 0;
    newShip.primaryEnemy = 2;

    // Encode it
    testee.makeShipCommands(33, oldShip, newShip);

    // Verify
    TS_ASSERT_EQUALS(testee.getNumCommands(), 2U);

    TurnFile::CommandCode_t code;
    TS_ASSERT(testee.getCommandCode(0, code));
    TS_ASSERT_EQUALS(code, uint32_t(game::v3::tcm_ShipChangeMission));
    TS_ASSERT(testee.getCommandCode(1, code));
    TS_ASSERT_EQUALS(code, uint32_t(game::v3::tcm_ShipChangePrimaryEnemy));

    // Verify size
    InternalStream out;
    testee.setFeatures(TurnFile::FeatureSet_t());
    testee.update();
    testee.write(out);

    // Expected file size: 29 (header) + 8 (command pos) + 6+6 (command data) + 256 (trailer) = 305
    TS_ASSERT_EQUALS(out.getSize(), 305U);
}

/** Test makePlanetCommands().
    A: create new turn file. Use makePlanetCommands().
    E: verify correct commands generated */
void
TestGameV3TurnFile::testMakePlanetCommands()
{
    // Empty turn file
    CodepageCharset cs(afl::charset::g_codepage437);
    Timestamp ts(2000, 12, 24, 18, 30, 0);
    TurnFile testee(cs, 7, ts);

    // Define a planet
    game::v3::structures::Planet oldPlanet;
    game::v3::structures::Planet newPlanet;
    afl::base::fromObject(oldPlanet).fill(0);
    afl::base::fromObject(newPlanet).fill(0);
    oldPlanet.planetId = 33;
    newPlanet.planetId = 33;
    oldPlanet.owner = 7;
    newPlanet.owner = 7;
    oldPlanet.numFactories = 1;
    newPlanet.numFactories = 20;
    oldPlanet.buildBaseFlag = 0;
    newPlanet.buildBaseFlag = 1;

    // Encode it
    testee.makePlanetCommands(33, oldPlanet, newPlanet);

    // Verify
    TS_ASSERT_EQUALS(testee.getNumCommands(), 2U);

    TurnFile::CommandCode_t code;
    TS_ASSERT(testee.getCommandCode(0, code));
    TS_ASSERT_EQUALS(code, uint32_t(game::v3::tcm_PlanetChangeFactories));
    TS_ASSERT(testee.getCommandCode(1, code));
    TS_ASSERT_EQUALS(code, uint32_t(game::v3::tcm_PlanetBuildBase));

    // Verify size
    InternalStream out;
    testee.setFeatures(TurnFile::FeatureSet_t());
    testee.update();
    testee.write(out);

    // Expected file size: 29 (header) + 8 (command pos) + 6+4 (command data) + 256 (trailer) = 303
    TS_ASSERT_EQUALS(out.getSize(), 303U);
}

/** Test makeBaseCommands().
    A: create new turn file. Use makeBaseCommands().
    E: verify correct commands generated */
void
TestGameV3TurnFile::testMakeBaseCommands()
{
    // Empty turn file
    CodepageCharset cs(afl::charset::g_codepage437);
    Timestamp ts(2000, 12, 24, 18, 30, 0);
    TurnFile testee(cs, 7, ts);

    // Define a base
    game::v3::structures::Base oldBase;
    game::v3::structures::Base newBase;
    afl::base::fromObject(oldBase).fill(0);
    afl::base::fromObject(newBase).fill(0);
    oldBase.baseId = 33;
    newBase.baseId = 33;
    oldBase.owner = 7;
    newBase.owner = 7;
    oldBase.mission = 0;
    newBase.mission = 5;
    oldBase.engineStorage[0] = 0;
    newBase.engineStorage[0] = 1;

    // Encode it
    testee.makeBaseCommands(33, oldBase, newBase);

    // Verify
    TS_ASSERT_EQUALS(testee.getNumCommands(), 2U);

    TurnFile::CommandCode_t code;
    TS_ASSERT(testee.getCommandCode(0, code));
    TS_ASSERT_EQUALS(code, uint32_t(game::v3::tcm_BaseBuildEngines));
    TS_ASSERT(testee.getCommandCode(1, code));
    TS_ASSERT_EQUALS(code, uint32_t(game::v3::tcm_BaseChangeMission));

    // Verify size
    InternalStream out;
    testee.setFeatures(TurnFile::FeatureSet_t());
    testee.update();
    testee.write(out);

    // Expected file size: 29 (header) + 8 (command pos) + 6+22 (command data) + 256 (trailer) = 312
    TS_ASSERT_EQUALS(out.getSize(), 321U);
}

/** Test modification of turn file header properties.
    A: load a turn file, modify and write out.
    E: new file reports new properties but otherwise same content. */
void
TestGameV3TurnFile::testModify()
{
    // Load existing turn file
    CodepageCharset cs(afl::charset::g_codepage437);
    NullTranslator tx;
    ConstMemoryStream ms(TURN35);
    TurnFile testee(cs, tx, ms);

    // Modify and write out again
    testee.setTimestamp(Timestamp(2000, 12, 24, 18, 30, 0));
    testee.setVersion(0);

    InternalStream out;
    testee.update();
    testee.write(out);

    // Size must not change
    TS_ASSERT_EQUALS(out.getSize(), ms.getSize());

    // Reload into new instance and compare
    out.setPos(0);
    TurnFile other(cs, tx, out);
    TS_ASSERT_EQUALS(other.getTimestamp().getTimestampAsString(), "12-24-200018:30:00");
    TS_ASSERT_EQUALS(other.getVersion(), 0);
}

/** Test sorting.
    A: create a fresh turn file with some commands. Call sortCommands().
    E: verify order */
void
TestGameV3TurnFile::testSort()
{
    // Empty turn file
    CodepageCharset cs(afl::charset::g_codepage437);
    Timestamp ts(2000, 12, 24, 18, 30, 0);
    TurnFile testee(cs, 7, ts);

    // Add some commands:
    static const uint8_t WORD_ARG[] = {0,0};
    static const uint8_t MSG_ARG[] = {'z'};
    testee.addCommand(game::v3::tcm_BaseChangeMission, 50, WORD_ARG);
    testee.addCommand(game::v3::tcm_BaseChangeMission, 60, WORD_ARG);
    testee.addCommand(game::v3::tcm_ShipChangeMission, 1, WORD_ARG);
    testee.addCommand(game::v3::tcm_ShipChangeMission, 100, WORD_ARG);
    testee.addCommand(game::v3::tcm_ShipChangePrimaryEnemy, 1, WORD_ARG);
    testee.addCommand(game::v3::tcm_ShipChangeSpeed, 1, WORD_ARG);
    testee.addCommand(game::v3::tcm_PlanetChangeFactories, 30, WORD_ARG);
    testee.sendMessageData(7, 5, MSG_ARG);
    testee.sendMessageData(7, 6, MSG_ARG);
    testee.addCommand(game::v3::tcm_BaseChangeMission, 70, WORD_ARG);

    // Sort
    testee.sortCommands();

    // Verify
    static const int EXPECT[][2] = {
        { game::v3::tcm_ShipChangeSpeed,        1 },
        { game::v3::tcm_ShipChangeMission,      1 },
        { game::v3::tcm_ShipChangePrimaryEnemy, 1 },
        { game::v3::tcm_ShipChangeMission,    100 },
        { game::v3::tcm_PlanetChangeFactories, 30 },
        { game::v3::tcm_BaseChangeMission,     50 },
        { game::v3::tcm_BaseChangeMission,     60 },
        { game::v3::tcm_BaseChangeMission,     70 },
        { game::v3::tcm_SendMessage,            1 },
        { game::v3::tcm_SendMessage,            1 },
    };
    TS_ASSERT_EQUALS(testee.getNumCommands(), countof(EXPECT));
    for (size_t i = 0; i < countof(EXPECT); ++i) {
        TurnFile::CommandCode_t cmd;
        TS_ASSERT(testee.getCommandCode(i, cmd));
        TS_ASSERT_EQUALS(int(cmd), EXPECT[i][0]);

        int id;
        TS_ASSERT(testee.getCommandId(i, id));
        TS_ASSERT_EQUALS(id, EXPECT[i][1]);
    }
}

/** Test setRegistrationKey().
    (This is a regression test mainly aimed at ports.)
    A: create a fresh turn file. Call setRegistrationKey() and write it out.
    E: verify against reference data */
void
TestGameV3TurnFile::testSetRegistrationKey()
{
    // Empty turn file
    CodepageCharset cs(afl::charset::g_codepage437);
    Timestamp ts(2000, 12, 24, 18, 30, 0);
    TurnFile testee(cs, 7, ts);

    // Set key
    RegistrationKey key(std::auto_ptr<Charset>(cs.clone()));
    key.initUnregistered();
    key.setLine(RegistrationKey::Line3, "Name");
    key.setLine(RegistrationKey::Line4, "Address");
    testee.setRegistrationKey(key, 500);

    // Write out
    InternalStream out;
    testee.update();
    testee.write(out);

    // Verify
    static const uint8_t REFERENCE[] = {
        0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0x32, 0x2d, 0x32, 0x34, 0x2d, 0x32, 0x30, 0x30, 0x30,
        0x31, 0x38, 0x3a, 0x33, 0x30, 0x3a, 0x30, 0x30, 0x00, 0x00, 0x85, 0x03, 0x56, 0x45, 0x52, 0x33,
        0x2e, 0x35, 0x30, 0x31, 0x47, 0x08, 0xaf, 0x1a, 0xbf, 0x05, 0x03, 0x62, 0x47, 0x60, 0x65, 0x96,
        0xa7, 0xa5, 0x15, 0x33, 0x5d, 0x07, 0x3f, 0xab, 0xb6, 0x92, 0x68, 0x9c, 0x5e, 0x65, 0x21, 0xf0,
        0x1a, 0xc6, 0xf9, 0xca, 0x1c, 0x11, 0x27, 0x24, 0xb6, 0xf7, 0xc9, 0x74, 0x5d, 0x38, 0x73, 0x4c,
        0x8b, 0xc5, 0xfa, 0x09, 0xee, 0x3b, 0x12, 0x40, 0x82, 0x7f, 0xe6, 0xd9, 0xea, 0x3c, 0xb2, 0x7f,
        0x26, 0x6d, 0x01, 0x2c, 0x21, 0x5b, 0x76, 0xe5, 0x21, 0x08, 0x22, 0x17, 0xbf, 0x9f, 0x89, 0xab,
        0xb1, 0x2f, 0x0e, 0x6d, 0x28, 0xfc, 0x79, 0xe2, 0x3c, 0x65, 0x4d, 0x48, 0x65, 0x01, 0x7b, 0x56,
        0xc5, 0x01, 0x28, 0x02, 0x37, 0x9f, 0xbf, 0xa9, 0x8b, 0x91, 0x0f, 0x2e, 0x4d, 0x08, 0xdc, 0x59,
        0x4e, 0x61, 0x6d, 0x65, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
        0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
        0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
        0x20, 0x20, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
        0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
        0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
        0x20, 0x20, 0x20, 0x20, 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, 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, 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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x4d, 0x00, 0x00, 0x43, 0x32, 0x4e, 0x47,
        0x5e, 0x04, 0x00, 0x00, 0x36, 0x07, 0x00, 0x00, 0xe7, 0x09, 0x00, 0x00, 0x80, 0x06, 0x00, 0x00,
        0x50, 0x14, 0x00, 0x00, 0xe8, 0x20, 0x00, 0x00, 0x7b, 0x22, 0x00, 0x00, 0xb0, 0x2c, 0x00, 0x00,
        0x29, 0x2e, 0x00, 0x00, 0xe8, 0x3a, 0x00, 0x00, 0x3d, 0x40, 0x00, 0x00, 0x80, 0x13, 0x00, 0x00,
        0xeb, 0x4b, 0x00, 0x00, 0xf0, 0x49, 0x00, 0x00, 0xe3, 0x49, 0x00, 0x00, 0xa0, 0x5c, 0x00, 0x00,
        0x31, 0x57, 0x00, 0x00, 0xc6, 0x6c, 0x00, 0x00, 0x97, 0x5d, 0x00, 0x00, 0xc8, 0x73, 0x00, 0x00,
        0xb5, 0x6b, 0x00, 0x00, 0xc0, 0x23, 0x00, 0x00, 0x60, 0x25, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00,
        0xa0, 0x28, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0xce, 0x06, 0x00, 0x00, 0x35, 0x0a, 0x00, 0x00,
        0x80, 0x06, 0x00, 0x00, 0x89, 0x12, 0x00, 0x00, 0x3e, 0x16, 0x00, 0x00, 0x60, 0x0b, 0x00, 0x00,
        0x00, 0x0d, 0x00, 0x00, 0xa0, 0x0e, 0x00, 0x00, 0x40, 0x10, 0x00, 0x00, 0xe0, 0x11, 0x00, 0x00,
        0x80, 0x13, 0x00, 0x00, 0x20, 0x15, 0x00, 0x00, 0xc0, 0x16, 0x00, 0x00, 0x60, 0x18, 0x00, 0x00,
        0x00, 0x1a, 0x00, 0x00, 0xa0, 0x1b, 0x00, 0x00, 0x40, 0x1d, 0x00, 0x00, 0xe0, 0x1e, 0x00, 0x00,
        0x80, 0x20, 0x00, 0x00, 0x20, 0x22, 0x00, 0x00, 0xc0, 0x23, 0x00, 0x00, 0x60, 0x25, 0x00, 0x00,
        0x00, 0x27, 0x00, 0x00, 0xa0, 0x28, 0x00, 0x00, 0x4b, 0x68, 0x07, 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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    };
    TS_ASSERT_EQUALS(out.getContent().size(), sizeof(REFERENCE));
    TS_ASSERT_SAME_DATA(out.getContent().unsafeData(), REFERENCE, sizeof(REFERENCE));
}

/** Test adding files.
    A: add a sequence of addFile(), deleteFile()
    E: verify results */
void
TestGameV3TurnFile::testAddFile()
{
    // Empty turn file
    CodepageCharset cs(afl::charset::g_codepage437);
    Timestamp ts(2000, 12, 24, 18, 30, 0);
    TurnFile testee(cs, 7, ts);

    // Add 5 files
    static const uint8_t DATA[] = {'x'};
    size_t pos;
    TS_ASSERT(testee.addFile(DATA, "one", pos));
    TS_ASSERT_EQUALS(pos, 0U);
    TS_ASSERT_EQUALS(testee.getFeatures(), TurnFile::FeatureSet_t() + TurnFile::TaccomFeature + TurnFile::WinplanFeature);
    TS_ASSERT(testee.addFile(DATA, "two", pos));
    TS_ASSERT_EQUALS(pos, 1U);
    TS_ASSERT(testee.addFile(DATA, "three", pos));
    TS_ASSERT_EQUALS(pos, 2U);
    TS_ASSERT(testee.addFile(DATA, "four", pos));
    TS_ASSERT_EQUALS(pos, 3U);
    TS_ASSERT(testee.addFile(DATA, "five", pos));
    TS_ASSERT_EQUALS(pos, 4U);
    TS_ASSERT_EQUALS(testee.getNumFiles(), 5U);

    // Remove one file
    testee.deleteFile(2);
    TS_ASSERT_EQUALS(testee.getNumFiles(), 4U);

    // Add one; this takes the free space
    TS_ASSERT(testee.addFile(DATA, "six", pos));
    TS_ASSERT_EQUALS(pos, 2U);

    // Add more
    TS_ASSERT(testee.addFile(DATA, "seven", pos));
    TS_ASSERT_EQUALS(pos, 5U);
    TS_ASSERT(testee.addFile(DATA, "eight", pos));
    TS_ASSERT_EQUALS(pos, 6U);
    TS_ASSERT(testee.addFile(DATA, "nine", pos));
    TS_ASSERT_EQUALS(pos, 7U);
    TS_ASSERT(testee.addFile(DATA, "ten", pos));
    TS_ASSERT_EQUALS(pos, 8U);
    TS_ASSERT(testee.addFile(DATA, "eleven", pos));
    TS_ASSERT_EQUALS(pos, 9U);
    TS_ASSERT_EQUALS(testee.getNumFiles(), 10U);

    // It's now full
    TS_ASSERT(!testee.addFile(DATA, "twelve", pos));
}

/** Test static command property access methods. */
void
TestGameV3TurnFile::testStatics()
{
    // Generic
    for (int i = 0; i < 1000; ++i) {
        if (TurnFile::getCommandCodeType(i) != TurnFile::UndefinedCommand) {
            TS_ASSERT(TurnFile::getCommandCodeName(i) != 0);
            if (TurnFile::getCommandCodeType(i) != TurnFile::OtherCommand) {
                TS_ASSERT(TurnFile::getCommandCodeRecordIndex(i) != 0);
            }
        }
    }

    // Specific
    TS_ASSERT_EQUALS(TurnFile::getCommandCodeType(game::v3::tcm_SendMessage), TurnFile::OtherCommand);
    TS_ASSERT_EQUALS(String_t(TurnFile::getCommandCodeName(game::v3::tcm_SendMessage)), "SendMessage");

    TS_ASSERT_EQUALS(TurnFile::getCommandCodeType(game::v3::tcm_ShipChangeWaypoint), TurnFile::ShipCommand);
    TS_ASSERT_EQUALS(String_t(TurnFile::getCommandCodeName(game::v3::tcm_ShipChangeWaypoint)), "ShipChangeWaypoint");

    TS_ASSERT_EQUALS(TurnFile::getCommandCodeType(game::v3::tcm_PlanetChangeFactories), TurnFile::PlanetCommand);
    TS_ASSERT_EQUALS(String_t(TurnFile::getCommandCodeName(game::v3::tcm_PlanetChangeFactories)), "PlanetChangeFactories");

    TS_ASSERT_EQUALS(TurnFile::getCommandCodeType(game::v3::tcm_BaseChangeDefense), TurnFile::BaseCommand);
    TS_ASSERT_EQUALS(String_t(TurnFile::getCommandCodeName(game::v3::tcm_BaseChangeDefense)), "BaseChangeDefense");
}
