#!/usr/bin/env python3
#
# Testing tool for the Adversarial Memory problem
#
# Usage:
#
#   python3 testing_tool.py [-n n] [-s seed] <program invocation>
#
# Use -n to specify the value of n, and -s to specify a fixed integer seed to reproduce testcases.
# If the -n parameter is not specified, a random n is generated.
# If the -s parameter is not specified, a random seed is used.
# Both n and the seed are printed at the start of each invocation. Specify
# those values using -n and -s to reproduce the run.
#
# For python, you could invoke this tool using
#
#   python3 testing_tool.py python3 my_submission.py
#
# For C++, you should first compile your code, and assuming this gives the binary a.out, use
#
#   python3 testing_tool.py ./a.out
#
# If you have a Java solution that you would run using
# "java MyClass", you could invoke the testing tool with:
#
#   python3 testing_tool.py java MyClass
#
# The tool is provided as-is, and you should feel free to make
# whatever alterations or augmentations you like to it.
#
# The tool attempts to detect and report common errors, but it is not an
# exhaustive test. It is not guaranteed that a program that passes this testing
# tool will be accepted.
#

import argparse
import subprocess
import sys
import traceback
import random

def write(p, line):
    assert p.poll() is None, 'Program terminated early'
    print('Write: {}'.format(line), flush=True)
    p.stdin.write('{}\n'.format(line))
    p.stdin.flush()


def read(p):
    assert p.poll() is None, 'Program terminated early'
    line = p.stdout.readline().strip()
    assert line != '', 'Read empty line or closed output pipe'
    print('Read: {}'.format(line), flush=True)
    return line


parser = argparse.ArgumentParser(description='Testing tool for problem Adversarial Memory.')
parser.add_argument('-n', dest='n', metavar='n', default=None, type=int,
                    help='The value of n to use. Will be randomly generated if not specified.')
parser.add_argument('-s', dest='seed', metavar='seed', default=None, type=int,
                    help='The seed for the random number generator. Will be random if not specified.')
parser.add_argument('program', nargs='+', help='Invocation of your solution')

args = parser.parse_args()

seed = args.seed
if seed is None:
    seed = random.randint(0, 2**31-1)
random.seed(seed)

n = args.n
if n is None:
    n = random.randint(1, 10000)
else:
    # (Not) specifying n does not change the rest of the program input.
    _ = random.randint(1, 10000)

assert 1 <= n and n <= 10000, 'n must be between 1 and 10000 inclusive.'

print('Log: n    = {}'.format(n), flush=True)
print('Log: seed = {}'.format(seed), flush=True)

with subprocess.Popen(" ".join(args.program), shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE,
                          universal_newlines=True) as p:
    try:
        write(p, n)

        values = [None] * (2*n+1)
        value_count = [0] * (2*n+1)
        done = [False] * (n+1)
        num_done = 0

        for turn in range(1, 2*n): # 1 ... 2n-1
            print('Log: Turn {}'.format(turn))
            # Pick two random distinct indices of not yet done cards.
            while True:
                i = random.randint(1, 2*n)
                if values[i] is None or not done[values[i]]:
                    break
            while True:
                j = random.randint(1, 2*n)
                if j == i: continue
                if values[i] is None or not done[values[i]]:
                    break

            # Ask the team submission for the value of the card at position i.
            write(p, i)
            value_i = int(read(p))
            # Ask the team submission for the value of the card at position j.
            write(p, j)
            value_j = int(read(p))

            assert 1 <= value_i and value_i <= n
            assert 1 <= value_j and value_j <= n

            if values[i] is not None:
                assert value_i == values[i], 'Value at position {} is not consistent'.format(i)
            if values[j] is not None:
                assert value_j == values[j], 'Value at position {} is not consistent'.format(j)

            if values[i] is None:
                values[i] = value_i
                value_count[value_i] += 1
                assert value_count[value_i] <= 2, 'Value {} seen at more than 2 positions'.format(i)
            if values[j] is None:
                values[j] = value_j
                value_count[value_j] += 1
                assert value_count[value_j] <= 2, 'Value {} seen at more than 2 positions'.format(j)

            if value_i == value_j:
                done[value_i] = True
                num_done += 1

            if num_done == n:
                assert turn >= 2*n-1, 'Game finished in less than 2*n-1 turns.'


        assert p.stdout.readline() == '', 'Your submission printed extra data after finding solution'
        assert p.wait() == 0, 'Your submission did not exit cleanly after finishing'

        sys.stdout.write('\nSuccess.\n'.format(p.wait()))
    except:
        print()
        traceback.print_exc()
        print()
        try:
            p.wait(timeout=2)
        except subprocess.TimeoutExpired:
            print('Killing your submission after 2 second timeout.')
            p.kill()
    finally:
        sys.stdout.flush()
        sys.stderr.flush()
        sys.stdout.write('Exit code: {}\n'.format(p.wait()))
        sys.stdout.flush()
