"""Tests for module exercises"""
import ast
from io import StringIO
from textwrap import dedent
import unittest
from unittest.mock import patch

from helpers import (
    run_program,
    import_module,
    make_file,
    redirect_stdout,
    ModuleTestCase,
)


class HowdyTests(ModuleTestCase):

    """
    Tests for howdy.py.

    Prompt user for their name and print a howdy greeting.
    """

    module_path = "howdy.py"

    def test_single_name(self):
        with patch("builtins.input", side_effect=["Trey"]) as mock_input:
            output = run_program("howdy.py")
        mock_input.assert_called_once_with("What's your name? ")
        self.assertEqual(output, "Howdy Trey!\n")

    def test_multiple_words(self):
        with patch("builtins.input", side_effect=["Trey Hunner"]) as mock_input:
            output = run_program("howdy.py")
        mock_input.assert_called_once_with("What's your name? ")
        self.assertEqual(output, "Howdy Trey Hunner!\n")

    def test_different_name(self):
        with patch("builtins.input", side_effect=["Emma"]) as mock_input:
            output = run_program("howdy.py")
        mock_input.assert_called_once_with("What's your name? ")
        self.assertEqual(output, "Howdy Emma!\n")


class TempTests(ModuleTestCase):

    """
    Tests for temp.py.

    Print out feedback on the given temperature.
    """

    module_path = "temp.py"

    def test_72_degrees(self):
        output = run_program("temp.py", ["72"])
        self.assertEqual(output, "Quite nice\n")

    def test_85_degrees(self):
        output = run_program("temp.py", ["85"])
        self.assertEqual(output, "Too hot\n")

    def test_60_degrees(self):
        output = run_program("temp.py", ["60"])
        self.assertEqual(output, "Too cold\n")

    def test_65_degrees(self):
        output = run_program("temp.py", ["66"])
        self.assertEqual(output, "Quite nice\n")

    def test_80_degrees(self):
        output = run_program("temp.py", ["80"])
        self.assertEqual(output, "Quite nice\n")


class DollarsTest(ModuleTestCase):

    """Tests for dollars.py."""

    module_path = "dollars.py"

    def test_twelve_dollars(self):
        self.assertEqual(run_program("dollars.py", ["12"]), "$12.00\n")

    def test_three_dollars_fourty(self):
        self.assertEqual(run_program("dollars.py", ["3.4"]), "$3.40\n")

    def test_three_cents(self):
        self.assertEqual(run_program("dollars.py", [".03"]), "$0.03\n")

    def test_fifty_cents(self):
        self.assertEqual(run_program("dollars.py", [".5"]), "$0.50\n")

    def test_three_decimals(self):
        self.assertEqual(run_program("dollars.py", [".008"]), "$0.01\n")


class AddTests(ModuleTestCase):

    """
    Tests for add.

    Add two numbers together.
    """

    module_path = "add.py"

    def test_integers(self):
        output = run_program("add.py", ["1", "2"])
        self.assertEqual(output, "3.0\n")

    def test_floats(self):
        output = run_program("add.py", ["1.5", "2"])
        self.assertEqual(output, "3.5\n")

    def test_negative_numbers(self):
        output = run_program("add.py", ["-7", "2"])
        self.assertEqual(output, "-5.0\n")
        output = run_program("add.py", ["-2", "-1"])
        self.assertEqual(output, "-3.0\n")

    def test_strings(self):
        with self.assertRaises(BaseException):
            run_program("add.py", ["hello", "hi"])


class MadlibTests(ModuleTestCase):

    """
    Tests for madlib.py.

    Create a Mad Libs word game that prompts for words and uses them in sentences.
    """

    module_path = "madlib.py"

    def test_first_three_words_used(self):
        test_input = "wackiest\npremise\noddest\nmuddy\nqueuing\n"
        with patch("builtins.input", side_effect=test_input.strip().split('\n')):
            output = run_program("madlib.py")

        # Check that the first three words appear in output
        self.assertIn("wackiest", output)
        self.assertIn("premise", output)
        self.assertIn("oddest", output)

    def test_different_input_words_used(self):
        test_input = "jumping\nquickly\nblue\ncat\nbig\n"
        with patch("builtins.input", side_effect=test_input.strip().split('\n')):
            output = run_program("madlib.py")

        # Check that words appear in output
        self.assertIn("jumping", output)
        self.assertIn("quickly", output)
        self.assertIn("blue", output)


class DifferenceTests(ModuleTestCase):

    """
    Tests for difference.

    Print the difference between two numbers.
    """

    module_path = "difference.py"

    def test_integers(self):
        output = run_program("difference.py", ["3", "5"])
        self.assertEqual(output, "2.0\n")

    def test_floats(self):
        output = run_program("difference.py", ["6", "3.5"])
        self.assertEqual(output, "2.5\n")

    def test_negative_numbers(self):
        output = run_program("difference.py", ["-7", "2"])
        self.assertEqual(output, "9.0\n")

    def test_strings(self):
        with self.assertRaises(BaseException):
            run_program("difference.py", ["hello", "hi"])


class SillyCaseTest(ModuleTestCase):

    """
    Tests for silly_case.py.

    Convert given string to "silly case" (lowercase followed by uppercase).
    """

    module_path = "silly.py"

    def test_silly_case(self):
        output = run_program("silly.py", ["Hello there!"])
        self.assertEqual(output, "hELLO tHERE!\n")


class GreetingsTests(ModuleTestCase):

    """
    Tests for greetings.

    Greet given user or greet the world if no arguments given.
    """

    module_path = "greetings.py"

    def test_no_arguments(self):
        output = run_program("greetings.py")
        self.assertEqual(output, "Hello world!\n")

    def test_with_argument(self):
        output = run_program("greetings.py", ["Trey"])
        self.assertEqual(output, "Hello Trey!\n")

    @unittest.skip("Comment this line to test multiple arguments")
    def test_with_multiple_arguments(self):
        output = run_program("greetings.py", ["Trey", "Hunner"])
        self.assertEqual(output, "Hello Trey Hunner!\n")


class AverageTests(ModuleTestCase):

    """
    Tests for average.

    Average all given numbers.
    """

    module_path = "average.py"

    def test_whole_number_result(self):
        output = run_program("average.py", ["2", "3", "4"])
        self.assertEqual(output, "Average is 3.0\n")

    def test_decimal_number_result(self):
        output = run_program("average.py", ["2", "3", "4", "5", "6", "7"])
        self.assertEqual(output, "Average is 4.5\n")

    @unittest.skip("Comment this line to test exception version.")
    def test_no_numbers(self):
        with self.assertRaises(SystemExit) as context:
            run_program("average.py")
        self.assertEqual(str(context.exception), "No numbers to average!")

    @unittest.skip("Comment this line to test exception version.")
    def test_strings(self):
        with self.assertRaises(SystemExit) as context:
            run_program("average.py", ["hello", "hi"])
        self.assertEqual(
            str(context.exception),
            "Invalid values entered, only numbers allowed!",
        )


class HelloTests(ModuleTestCase):

    """
    Tests for hello.

    Print hello world when run from command-line but not when imported.
    """

    module_path = "hello.py"

    def test_cli(self):
        output = run_program("hello.py")
        self.assertEqual(output, "Hello world!\n")

    # @unittest.skip("Un-comment this line for CLI-only exception exercise")
    def test_import(self):
        with redirect_stdout(StringIO()) as output:
            import_module("hello")
        self.assertIn("command-line", output.getvalue())

    @unittest.skip("Comment out this line for CLI-only exception exercise")
    def test_exception_on_import(self):
        with self.assertRaises(ImportError) as cm:
            import_module("hello")
        self.assertIn("command-line", str(cm.exception))


class WCTests(ModuleTestCase):

    """
    Tests for wc.

    Print newline, word, and letter counts for given files.

    """

    module_path = "wc.py"

    def test_ask_version(self):
        output, error = run_program('wc.py', ['--version'], stderr=True)
        self.assertEqual(output, 'Word Count 1.0\n')
        self.assertEqual(error, '')

    def test_character(self):
        output = run_program('wc.py', ['-c', 'us-state-capitals.csv'])
        self.assertEqual(output, 'us-state-capitals.csv:\nchars: 953\n')

    def test_character_line(self):
        output = run_program('wc.py', ['-cl', 'us-state-capitals.csv'])
        self.assertEqual(output, 'us-state-capitals.csv:\nlines: 51\nchars: 953\n')

    def test_declaration_words(self):
        output = run_program('wc.py', ['-w', 'declaration-of-independence.txt'])
        self.assertEqual(output, 'declaration-of-independence.txt:\nwords: 1338\n')

    def test_default(self):
        output = run_program('wc.py', ['declaration-of-independence.txt'])
        self.assertEqual(output, 'declaration-of-independence.txt:\nlines: 67\nwords: 1338\nchars: 8190\n')

    def test_character_line_separate(self):
        output = run_program('wc.py', ['-l', '-c', 'us-state-capitals.csv'])
        self.assertEqual(output, 'us-state-capitals.csv:\nlines: 51\nchars: 953\n')

    def test_multiple(self):
        output = run_program('wc.py', ['-c', 'declaration-of-independence.txt',
                            'us-state-capitals.csv'])
        self.assertEqual(output, 'declaration-of-independence.txt:\nchars: 8190\nus-state-capitals.csv:\nchars: 953\n')

    def test_help_message(self):
        expected_output = dedent("""
            usage: wc [-h] [-c] [-w] [-l] [--version] files
            [files ...]

            positional arguments:
            files

            optional arguments:

            -h, --help   show this help message and exit
            -c, --chars  print the
            character count
            -w, --words  print the word count
            -l, --lines
            print the newline count
            --version    show program's version number
            and exit
        """
        )
        output = run_program("wc.py", ["-h"])
        self.assertIn("--help", output)
        self.assertIn("--chars", output)
        self.assertIn("--words", output)
        self.assertIn("--lines", output)
        self.assertIn("--version", output)
        self.assertIn("-c", output)
        self.assertIn("-w", output)
        self.assertIn("-l", output)


class ConvertCSVTests(ModuleTestCase):

    """
    Tests for convert_csv.

    Outputs a CSV file with a different delimiter than the one given

    """

    module_path = "convert_csv.py"

    def test_single_column(self):
        inputs = [
            "line1",
            "line2",
            "line3",
        ]
        with make_file("\n".join(inputs)) as file1, make_file("") as file2:
            args = ["--in-delim=\t", "--out-delim=,", file1, file2]
            run_program("convert_csv.py", args)
            with open(file2) as csv_file:
                outputs = csv_file.read().splitlines()
        self.assertEqual(inputs, outputs)

    def test_two_columns(self):
        inputs = [
            "a,b",
            "c,d",
            "e,f",
        ]
        expected = [
            "a\tb",
            "c\td",
            "e\tf",
        ]
        with make_file("\n".join(inputs)) as file1, make_file("") as file2:
            args = ["--in-delim=,", "--out-delim=\t", file1, file2]
            run_program("convert_csv.py", args)
            with open(file2) as csv_file:
                outputs = csv_file.read().splitlines()
        self.assertEqual(outputs, expected)

    def test_quotes_and_delimeter_usage(self):
        inputs = [
            "oh what a day, what a lovely day",
            "this line has two columns\tyes 2",
            '"this line has \t in quotes"',
        ]
        expected = [
            '"oh what a day, what a lovely day"',
            "this line has two columns,yes 2",
            "this line has \t in quotes",
        ]
        with make_file("\n".join(inputs)) as file1, make_file("") as file2:
            args = ["--in-delim=\t", "--out-delim=,", file1, file2]
            run_program("convert_csv.py", args)
            with open(file2) as csv_file:
                outputs = csv_file.read().splitlines()
        self.assertEqual(outputs, expected)


class PhoneticTests(ModuleTestCase):

    """
    Tests for phonetic.py.

    Print out spelling of words in NATO phonetic alphabet.
    """

    module_path = "phonetic.py"

    def test_all_lowercase(self):
        output = run_program("phonetic.py", ["python"])
        self.assertEqual(
            output,
            "Papa\nYankee\nTango\nHotel\nOscar\nNovember\n",
        )

    def test_all_uppercase(self):
        output = run_program("phonetic.py", ["PYTHON"])
        self.assertEqual(
            output,
            "Papa\nYankee\nTango\nHotel\nOscar\nNovember\n",
        )

    def test_mixed_case(self):
        output = run_program("phonetic.py", ["PytHOn"])
        self.assertEqual(
            output,
            "Papa\nYankee\nTango\nHotel\nOscar\nNovember\n",
        )

    def test_ignore_non_letters(self):
        output = run_program("phonetic.py", ["Python!"])
        self.assertEqual(
            output,
            "Papa\nYankee\nTango\nHotel\nOscar\nNovember\n",
        )

    def test_multiple_words(self):
        output = run_program("phonetic.py", ["Monty Python"])
        self.assertEqual(
            output,
            "Mike\nOscar\nNovember\nTango\nYankee\n" +
            "\n" +
            "Papa\nYankee\nTango\nHotel\nOscar\nNovember\n",
        )


class GuessIn3Tests(ModuleTestCase):

    """
    Tests for guess.py.

    Interactive guessing game refactored to use loops.
    """

    module_path = "guess.py"

    def assertBetween(self, value, low, high, message=""):
        self.assertGreaterEqual(value, low, message)
        self.assertLessEqual(value, high, message)

    def test_eventually_correct(self):
        for _ in range(1000):
            with patch("builtins.input", side_effect=["3", "4", "5"]):
                output = run_program("guess.py")
                if "Correct!" in output:
                    break
        self.assertIn(
            "Correct!",
            output,
            "\n\nFor 1,000 runs, 'Correct!' was never displayed.",
        )

    def test_eventually_incorrect(self):
        for _ in range(1000):
            with patch("builtins.input", side_effect=["3", "4", "5"]):
                output = run_program("guess.py")
                if "Correct!" not in output:
                    break
        self.assertNotIn(
            "Correct!",
            output,
            "\n\nFor 1,000 runs, 'Correct!' was always displayed.",
        )
        # Should have made all three attempts
        self.assertIn("Too", output)  # Should see "Too low" or "Too high"
        self.assertIn("Incorrect.", output)

    def test_same_number_is_always_either_correct_or_not(self):
        outputs = []
        for _ in range(1000):
            with patch("builtins.input", side_effect=["3", "3", "3"]):
                outputs.append(run_program("guess.py"))

        correct_guesses = self.countContaining(outputs, "Correct!")/10
        self.assertBetween(
            correct_guesses,
            7,
            18,
            "\n\nExpected 1 of 8 guesses (~12.5%) are correct.",
        )

        has_incorrect_guesses = self.countContaining(outputs, "Incorrect.")/10
        self.assertBetween(
            has_incorrect_guesses,
            82,
            93,
            "\n\nExpected 7 of 8 guesses (~87.5%) are incorrect.",
        )

        # Roughly 7/8 times, the first guess should be wrong and we should see "Too"
        at_least_feedback = self.countContaining(outputs, "Too")/10
        self.assertBetween(at_least_feedback, 82, 93)

    def test_different_numbers_guessed(self):
        outputs = []
        for _ in range(1000):
            with patch("builtins.input", side_effect=["3", "2", "1"]):
                outputs.append(run_program("guess.py"))

        correct_guesses = self.countContaining(outputs, "Correct!")/10
        self.assertBetween(
            correct_guesses,
            32,
            43,
            "\n\nExpected 3 of 8 (~37.5%) of runs have correct guess.",
        )

        # 5/8 chance that all of the guesses are incorrect
        has_incorrect_guesses = self.countContaining(outputs, "Incorrect.")/10
        self.assertBetween(
            has_incorrect_guesses,
            57,
            68,
            "\n\nExpected 5 of 8 (~62.5%) of runs have all incorrect guesses.",
        )

        # Verify that "Too" feedback is given when guess is wrong
        has_feedback = self.countContaining(outputs, "Too")/10
        self.assertBetween(
            has_feedback,
            57,
            100,
            "\n\nExpected most runs to provide feedback when guesses are wrong."
        )

    def test_loop_used(self):
        from pathlib import Path
        DIRECTORY = Path(__file__).resolve().parent
        tree = ast.parse((DIRECTORY / "modules" / "guess.py").read_text())
        loop_nodes = [
            node
            for node in ast.walk(tree)
            if isinstance(node, ast.For)
        ]
        prints_in_loops = [
            node
            for loop_node in loop_nodes
            for node in ast.walk(loop_node)
            if isinstance(node, ast.Call) and hasattr(node.func, 'id') and node.func.id == "print"
        ]
        self.assertGreaterEqual(
            len(loop_nodes),
            1,
            "Expected at least one loop",
        )
        self.assertGreaterEqual(
            len(prints_in_loops),
            1,
            "Expected at least one print in a loop",
        )

    def countContaining(self, strings, substring):
        """Return number of strings containing a given substring."""
        return sum(
            substring in s
            for s in strings
        )


if __name__ == "__main__":
    from helpers import error_message

    error_message()
