import os
import re
import subprocess
import tempfile
import unittest

import torch
import torch._dynamo
import torch._dynamo.test_case
from torch._dynamo.debug_utils import TEST_REPLACEABLE_COMMENT


class MinifierTestBase(torch._dynamo.test_case.TestCase):
    _debug_dir_obj = tempfile.TemporaryDirectory()
    DEBUG_DIR = _debug_dir_obj.name

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls._exit_stack.enter_context(
            unittest.mock.patch.object(
                torch._dynamo.config,
                "debug_dir_root",
                cls.DEBUG_DIR,
            )
        )
        os.makedirs(cls.DEBUG_DIR, exist_ok=True)

    @classmethod
    def tearDownClass(cls):
        cls._debug_dir_obj.cleanup()
        cls._exit_stack.close()

    # Search for the name of the first function defined in a code string.
    def _get_fn_name(self, code):
        fn_name_match = re.search(r"def (\w+)\(", code)
        if fn_name_match is not None:
            return fn_name_match.group(1)
        return None

    # Run `code` in a separate python process.
    # Returns the completed process state and the directory containing the
    # minifier launcher script, if `code` outputted it.
    def _run_test_code(self, code):
        proc = subprocess.run(
            ["python3", "-c", code], capture_output=True, cwd=self.DEBUG_DIR
        )
        repro_dir_match = re.search(
            r"(\S+)minifier_launcher.py", proc.stderr.decode("utf-8")
        )
        if repro_dir_match is not None:
            return proc, repro_dir_match.group(1)
        return proc, None

    # Patch generated files with testing patches
    def _inject_code(self, patch_code, filename):
        patch_code = f"""\
{patch_code}
torch._dynamo.config.debug_dir_root = "{self.DEBUG_DIR}"
"""
        with open(filename, "r") as f:
            code = f.read()
        code = code.replace(TEST_REPLACEABLE_COMMENT, patch_code)
        with open(filename, "w") as f:
            f.write(code)
        return code

    # Runs the minifier launcher script in `repro_dir`, patched with `patch_code`.
    def _run_minifier_launcher(self, patch_code, repro_dir):
        self.assertIsNotNone(repro_dir)
        launch_file = os.path.join(repro_dir, "minifier_launcher.py")
        self.assertTrue(os.path.exists(launch_file))
        launch_code = self._inject_code(patch_code, launch_file)

        launch_proc = subprocess.run(
            ["python3", launch_file],
            capture_output=True,
            cwd=repro_dir,
        )

        return launch_proc, launch_code

    # Runs the repro script in `repro_dir`, patched with `patch_code`
    def _run_repro(self, patch_code, repro_dir):
        self.assertIsNotNone(repro_dir)
        repro_file = os.path.join(repro_dir, "repro.py")
        self.assertTrue(os.path.exists(repro_file))
        repro_code = self._inject_code(patch_code, repro_file)

        repro_proc = subprocess.run(
            ["python3", repro_file], capture_output=True, cwd=repro_dir
        )
        return repro_proc, repro_code

    # Template for testing code.
    # `run_code` is the code to run for the test case.
    # `patch_code` is the code to be patched in every generated file.
    def _gen_test_code(self, run_code, repro_after, repro_level, patch_code):
        return f"""\
import torch
import torch._dynamo
{patch_code}
torch._dynamo.config.repro_after = "{repro_after}"
torch._dynamo.config.repro_level = {repro_level}
torch._dynamo.config.debug_dir_root = "{self.DEBUG_DIR}"
{run_code}
"""

    # Runs a full minifier test.
    # Minifier tests generally consist of 3 stages:
    # 1. Run the problematic code (in a separate process since it could segfault)
    # 2. Run the generated minifier launcher script
    # 3. Run the generated repro script
    def _run_full_test(self, run_code, repro_after, repro_level, patch_code):
        test_code = self._gen_test_code(run_code, repro_after, repro_level, patch_code)
        test_proc, repro_dir = self._run_test_code(test_code)
        self.assertIsNotNone(repro_dir)
        launch_proc, launch_code = self._run_minifier_launcher(patch_code, repro_dir)
        repro_proc, repro_code = self._run_repro(patch_code, repro_dir)
        return ((test_proc, launch_proc, repro_proc), (launch_code, repro_code))
