#!/usr/bin/env python3
#
# Testing tool for the Balloons and Dubloons problem
#
# Usage:
#
#   python3 testing_tool.py -f inputfile <program invocation>
#
#
# Use the -f parameter to specify the input file, e.g. 1.in.
# The input file should contain the following:
# - The first line contains "captain".
# - The second line contains two integers h and w, the number of rows and columns of the map, respectively.
# - The following h lines each contain w characters describing the treasure map:
#   - A character ~ indicates water.
#   - A character . indicates unmarked land.
#   - A character X indicates a treasure marker.
#   - A character # indicates a treasure marker pointing to the actual treasure.

# You can compile and run your solution as follows:

# C++:
#   g++ solution.cpp
#   python3 testing_tool.py -f 1.in ./a.out

# Python:
#   python3 testing_tool.py -f 1.in python3 ./solution.py

# Java:
#   javac solution.java
#   python3 testing_tool.py -f 1.in java solution

# Kotlin:
#   kotlinc solution.kt
#   python3 testing_tool.py -f 1.in kotlin solutionKt


# 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 traceback

parser = argparse.ArgumentParser(description="Testing tool for problem Balloons and Dubloons.")
parser.add_argument(
    "-f",
    dest="inputfile",
    metavar="inputfile",
    default=None,
    type=argparse.FileType("r"),
    required=True,
    help="The input file to use.",
)
parser.add_argument("program", nargs="+", help="Invocation of your solution")

args = parser.parse_args()


def single_pass(role: str, height: int, width: int, treasure_map: list[str]) -> list[str]:
    with (
        subprocess.Popen(
            " ".join(args.program),
            shell=True,
            stdout=subprocess.PIPE,
            stdin=subprocess.PIPE,
            universal_newlines=True,
        ) as p,
    ):
        assert p.stdin is not None and p.stdout is not None

        raw = f"{role}\n{height} {width}\n" + "\n".join(treasure_map) + "\n"
        stdout, stderr = p.communicate(input=raw)
        output = [line.strip() for line in stdout.strip().split("\n") if line.strip()]

        assert len(output) == height, f"Received an output map of {len(output)} rows instead of the expected height of {height}"
        assert all(len(row) == width for row in output), f"At least one row of the output does not have the specified width of {width}"
        assert all(all(c in ".~X#" for c in row) for row in output), f"The output contains characters that are not . ~ X or #"

        differing_characters = 0
        for r in range(height):
            for c in range(width):
                if output[r][c] != treasure_map[r][c]:
                    differing_characters += 1
                    if role == "captain":
                        assert output[r][c] == "X" and treasure_map[r][c] == ".", f"First-pass output changed {treasure_map[r][c]} into {output[r][c]} instead of marking an unmarked piece of land"
                    if role == "matey":
                        assert output[r][c] == "#" and treasure_map[r][c] == "X", f"Second-pass output changed {treasure_map[r][c]} into {output[r][c]} instead of marking one of the treasure markers as correct"

        assert differing_characters == 1, f"The output differs from the input in {differing_characters} places, instead of the expected single change"

        return output


try:
    with args.inputfile as f:
        # Parse input
        lines = [line.strip() for line in f.readlines()]
        role = lines[0]
        height, width = list(map(int, lines[1].split()))
        treasure_map = lines[2:]

        assert role == "captain", f"Initial role must be 'captain', but got {role}"
        assert len(treasure_map) == height, f"Treasure map contains {len(treasure_map)} rows, but height was specified as {height}"
        assert all(len(row) == width for row in treasure_map), f"Not all rows of the treasure map have the specified width of {width}"
        assert all(all(c in ".~X#" for c in row) for row in treasure_map), f"The input contains characters that are not . ~ X or #"

    print("Running first pass (captain)")
    captains_map = single_pass("captain", height, width, treasure_map)
    captains_map_string = "\n".join(captains_map)

    hidden_map = [row.replace("#", 'X') for row in captains_map]

    print("Running second pass (matey)")
    mateys_map = single_pass("matey", height, width, hidden_map)
    mateys_map_string = "\n".join(row.strip() for row in mateys_map if row.strip())

    assert mateys_map_string == captains_map_string, f"The second-pass output mismatched the first-pass output.\n\nFirst pass output:\n{captains_map_string}\n\nSecond pass output:\n{mateys_map_string}"

    print("Success.")

except AssertionError as e:
    print()
    print(f"Error: {e}")
    print()
    exit(1)

except Exception:
    print()
    print("Unexpected error:")
    traceback.print_exc()
    print()
    exit(1)
