import os
import shutil
from pathlib import Path
from tempfile import mkdtemp
from psychopy.experiment import getAllComponents, Experiment
from psychopy.tests.utils import compareTextFiles, TESTS_DATA_PATH
from psychopy.scripts import psyexpCompile
from psychopy import constants


class _TestBoilerplateMixin:
    """
    Mixin for tests of classes in the PsychoPy library to check they are able to work with the compiled code from
    Builder.
    """

    obj = None

    def test_input_params(self):
        """
        All classes called from boilerplate should accept name and autoLog as input params
        """
        if self.obj is None:
            return
        # Define list of names which need to be accepted by init
        required = (
            "name",
            "autoLog"
        )
        # Get names of input variables
        varnames = type(self.obj).__init__.__code__.co_varnames
        # Make sure required names are accepted
        for name in required:
            assert name in varnames, (
                f"{type(self.obj)} init function should accept {name}, but could not be found in list of kw args."
            )

    def test_status(self):
        """
        All classes called from boilerplate should have a settable status attribute which accepts psychopy constants
        """
        if self.obj is None:
            return
        # Check that status can be NOT_STARTED without error
        self.obj.status = constants.NOT_STARTED
        # Check that status can be STARTED without error
        self.obj.status = constants.STARTED
        # Check that status can be FINISHED without error
        self.obj.status = constants.FINISHED

        # Set back to NOT_STARTED for other tests
        self.obj.status = constants.NOT_STARTED


class TestComponentCompilerPython():
    """A class for testing the Python code compiler for all components"""
    def setup(self):
        self.temp_dir = mkdtemp()
        self.allComp = getAllComponents(fetchIcons=False)
        self.exp = Experiment() # create once, not every test
        self.exp.addRoutine('trial')
        self.exp.flow.addRoutine(self.exp.routines['trial'], pos=0)
        # Create correctScript subdir for holding correct scripts
        if not os.path.isdir(os.path.join(TESTS_DATA_PATH, "correctScript", "python")):
            os.mkdir(os.path.join(TESTS_DATA_PATH, "correctScript", "python"))

    def teardown(self):
        shutil.rmtree(self.temp_dir)

    def test_all_components(self):
        """Test all component code outputs, except for Settings and Unknown"""
        for compName in self.allComp:
            if compName not in ['SettingsComponent', 'UnknownComponent']:
                # reset exp
                self.reset_experiment()
                # Add components
                self.add_components(compName)
                # Create output script
                self.create_component_output(compName)
                # Get correct script path
                # correctPath = os.path.join(TESTS_DATA_PATH, "correctScript", "python", 'correct{}.py'.format(compName))
                # Compare files, raising assertions on fails above tolerance (%)
                # try:
                #     compareTextFiles('new{}.py'.format(compName), correctPath, tolerance=5)
                # except IOError as err:
                #     compareTextFiles('new{}.py'.format(compName), correctPath, tolerance=5)

    def reset_experiment(self):
        """Resets the exp object for each component"""
        self.exp = Experiment()
        self.exp.addRoutine('trial')
        self.exp.flow.addRoutine(self.exp.routines['trial'], pos=0)

    def add_components(self, compName):
        """Add components to routine"""
        thisComp = self.allComp[compName](parentName='trial', exp=self.exp)
        if compName == 'StaticComponent':
            # Create another component to trigger param updates for static
            textStim = self.allComp['TextComponent'](parentName='trial', exp=self.exp)
            textStim.params['color'].allowedUpdates.append('set during: trial.ISI')
            textStim.params['color'].updates = 'set during: trial.ISI'
            self.exp.routines['trial'].addComponent(textStim)
            # Create static component
            thisComp.addComponentUpdate('trial', 'text', 'color')
            thisComp.params['code'].val = "customStaticCode = True"  # Add the custom code
            self.exp.routines['trial'].addComponent(thisComp)
        else:
            self.exp.routines['trial'].addComponent(thisComp)

    def create_component_output(self, compName):
        """Create the Python script"""
        pyFilePath = os.path.join(self.temp_dir, 'new{}.py'.format(compName))
        psyexpCompile.compileScript(infile=self.exp, outfile=pyFilePath)

    def test_component_type_in_experiment(self):
        for compName, compObj in self.allComp.items():
            if compName not in ['SettingsComponent', 'UnknownComponent'] and "PsychoPy" in compObj.targets:
                # reset exp
                self.reset_experiment()
                # Add components
                self.add_components(compName)
                # Check component in exp
                component = compName.split('Component')[0]
                assert self.exp.getComponentFromType(component), (
                    f"Could not find component of type {compName} in: {self.exp.flow}"
                )
