from io import StringIO
from itertools import zip_longest
from pathlib import Path
from subprocess import PIPE, run
from sys import stderr
from warnings import warn

from py2puml.controller import InspectorController


def assert_py2puml_ios(py2puml_contents: StringIO, expected_py2puml_contents: StringIO, source: str):
    """
    Compares actual against expected IO contents, typically the plantuml contents generated by a py2puml call against file contents.

    Args:
        py2puml_contents: contents streamed typically from a call to py2puml inspection
        expected_py2puml_contents: expected contents streamed typically from an opened file
        source: an arbitrary text explaining how the py2puml_contents were created (a py2puml call)

    Raises:
        ValueError: on the first line where the contents are different. The error message includes the line index.
    """
    for line_index, (output_line, expected_output_line) in enumerate(
        zip_longest(py2puml_contents, expected_py2puml_contents, fillvalue='<end of contents>'), start=1
    ):
        output_line = output_line.strip('\n')
        expected_output_line = expected_output_line.strip('\n')
        # print(f"{line_index=}", f"{output_line=}", f"{expected_output_line=}")
        if output_line != expected_output_line:
            raise ValueError(
                f"Line {line_index} is '{output_line}' but '{expected_output_line}' was expected (source: '{source}')"
            )


def assert_py2puml_command_args(
    command_args: str,
    expected_output_file_path: Path,
    current_working_directory: str = None,
    *,
    overwrite_expected_output: bool = False,
):
    """
    Runs a py2puml command in a subprocess and compares its output with the contents of the given output file.

    Args:
        command_args: the command you want to run, without the leading py2puml part
        expected_output_file_path: the path of the expected contents
        current_working_directory: the working directory where this command should be run. Leave to None if the current working directory is fine.
        overwrite_expected_output: set to true to update the file containing the expected contents

    Raises:
        ValueError: on the first line where the contents are different. The error message includes the line index.
    """

    if command_args.startswith('py2puml ') or command_args.startswith('python'):
        raise ValueError(
            f"The command should contain only the arguments without reference to the py2puml executable, got '{command_args}'"
        )

    # runs the py2puml command
    command_args_parts = command_args.split(' ')
    cli_process = run(
        ['py2puml', *command_args_parts],
        stdout=PIPE,
        stderr=PIPE,
        text=True,
        check=False,
        cwd=current_working_directory,
    )
    py2puml_command = f'py2puml {command_args}'
    if process_stderr := cli_process.stderr:
        stderr.writelines(process_stderr.splitlines(keepends=True))
        raise RuntimeError(f"An error occurred when running '{py2puml_command}'")
    parsed_args = InspectorController()._parse_args(command_args_parts)

    # verifies the generated contents, either in the written file or in the captured stdout
    expected_output_file_open_mode = 'w' if overwrite_expected_output else 'r'
    overwrite_warning = f"assert_py2puml_command_args() is used to overwrite file '{expected_output_file_path}'"
    if parsed_args.output_file:
        with (
            open(parsed_args.output_file, encoding='utf-8') as output_file,
            open(expected_output_file_path, expected_output_file_open_mode, encoding='utf-8') as expected_output_file,
        ):
            if overwrite_expected_output:
                expected_output_file.writelines(output_file)
                warn(overwrite_warning, UserWarning, stacklevel=1)
            else:
                assert_py2puml_ios(output_file, expected_output_file, py2puml_command)
    else:
        # removes the last return carriage added by the stdout
        output_io = StringIO(cli_process.stdout[:-1])

        with open(expected_output_file_path, expected_output_file_open_mode, encoding='utf-8') as expected_output_file:
            if overwrite_expected_output:
                expected_output_file.writelines(output_io)
                warn(overwrite_warning, UserWarning, stacklevel=1)
            else:
                assert_py2puml_ios(output_io, expected_output_file, py2puml_command)
