#  Copyright 2008-2015 Nokia Solutions and Networks
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

import os
from itertools import chain
import shutil
import commands
from robotide.controller.dataloader import ExcludedDirectory, TestData

from robotide.publish import (RideDataFileRemoved, RideInitFileRemoved,
        RideDataChangedToDirty, RideDataDirtyCleared, RideSuiteAdded,
        RideItemSettingsChanged)
from robotide.publish.messages import RideDataFileSet, RideOpenResource
from robotide.robotapi import TestDataDirectory, TestCaseFile, ResourceFile
from robotide import utils

from .basecontroller import WithUndoRedoStacks, _BaseController, WithNamespace, ControllerWithParent
from .macrocontrollers import UserKeywordController
from .robotdata import NewTestCaseFile, NewTestDataDirectory
from robotide.utils import overrides
from .settingcontrollers import (DocumentationController, FixtureController,
        TimeoutController, TemplateController, DefaultTagsController,
        ForceTagsController)
from .tablecontrollers import (VariableTableController, TestCaseTableController,
        KeywordTableController, ImportSettingsController,
        MetadataListController, TestCaseController)


def _get_controller(project, data, parent):
    if isinstance(data, TestCaseFile):
        return TestCaseFileController(data, project, parent)
    if isinstance(data, ExcludedDirectory):
        return ExcludedDirectoryController(data, project, parent)
    return TestDataDirectoryController(data, project, parent)


def DataController(data, project, parent=None):
    return _get_controller(project, data, parent)


class _FileSystemElement(object):

    def __init__(self, filename, directory):
        self.filename = filename
        self.directory = directory
        self._stat = self._get_stat(filename)

    def _get_stat(self, path):
        if path and os.path.isfile(path):
            stat = os.stat(path)
            return stat.st_mtime, stat.st_size
        return 0, 0

    def refresh_stat(self):
        self._stat = self._get_stat(self.filename)

    def has_been_modified_on_disk(self):
        return self._get_stat(self.filename) != self._stat

    def has_been_removed_from_disk(self):
        return self._stat != (0, 0) and not self.exists()

    def relative_path_to(self, other):
        other_path = os.path.join(other.directory, other.filename)
        return os.path.relpath(other_path, start=self.directory).replace('\\', '/')

    def is_readonly(self):
        return not os.access(self.filename, os.W_OK)

    def exists(self):
        return self.filename and os.path.isfile(self.filename)

    @property
    def basename(self):
        return os.path.splitext(os.path.basename(self.filename))[0]

    @property
    def source(self):
        """Deprecated, use ``filename`` or ``directory`` instead."""
        # Todo: remove when backwards compatibility with plugin API can break
        return self.filename or self.directory


class _DataController(_BaseController, WithUndoRedoStacks, WithNamespace):

    def __init__(self, data, project=None, parent=None):
        self._project = project
        if project:
            self._set_namespace_from(project)
            self._resource_file_controller_factory =\
                project.resource_file_controller_factory
        else:
            self._resource_file_controller_factory = None
        self.parent = parent
        self.set_datafile(data)
        self.dirty = False
        self.children = self._children(data)

    def set_datafile(self, datafile):
        self.data = datafile
        self._variables_table_controller = None
        self._testcase_table_controller = None
        self._keywords_table_controller = None
        self._imports = None
        RideDataFileSet(item=self).publish()

    def _children(self, data):
        return []

    @property
    def name(self):
        return self.data.name

    @property
    def settings(self):
        return self._settings()

    def _settings(self):
        ss = self.data.setting_table
        return [DocumentationController(self, ss.doc),
                FixtureController(self, ss.suite_setup),
                FixtureController(self, ss.suite_teardown),
                FixtureController(self, ss.test_setup),
                FixtureController(self, ss.test_teardown),
                self.force_tags]

    @property
    def _setting_table(self):
        return self.data.setting_table

    @property
    def force_tags(self):
        return ForceTagsController(self, self._setting_table.force_tags)

    @property
    def variables(self):
        if self._variables_table_controller is None:
            self._variables_table_controller = \
                    VariableTableController(self, self.data.variable_table)
        return self._variables_table_controller

    @property
    def tests(self):
        if self._testcase_table_controller is None:
            self._testcase_table_controller = \
                    TestCaseTableController(self, self.data.testcase_table)
        return self._testcase_table_controller

    @property
    def datafile(self):
        return self.data

    @property
    def datafiles(self):
        return chain([self], (df for df in self._project.datafiles
                              if df != self))

    @property
    def datafile_controller(self):
        return self

    @property
    def keywords(self):
        if self._keywords_table_controller is None:
            self._keywords_table_controller = \
                    KeywordTableController(self, self.data.keyword_table)
        return self._keywords_table_controller

    @property
    def imports(self):
        if not self._imports:
            self._imports = ImportSettingsController(self, self.data.setting_table,
                                    self._resource_file_controller_factory)
        return self._imports

    @property
    def metadata(self):
        return MetadataListController(self, self.data.setting_table)

    def is_user_keyword(self, value):
        return WithNamespace.is_user_keyword(self, self.datafile, value)

    def is_library_keyword(self, value):
        return WithNamespace.is_library_keyword(self, self.datafile, value)

    def keyword_info(self, keyword_name):
        return WithNamespace.keyword_info(self, self.data, keyword_name)

    def mark_dirty(self):
        if not self.dirty:
            self.dirty = True
            RideDataChangedToDirty(datafile=self).publish()

    def unmark_dirty(self):
        self.refresh_stat()
        if self.dirty:
            self.dirty = False
            RideDataDirtyCleared(datafile=self).publish()

    def create_keyword(self, name, argstr=''):
        return self.keywords.new(name, argstr)

    def add_test_or_keyword(self, item):
        if isinstance(item, TestCaseController):
            self.tests.add(item)
            item.set_parent(self.tests)
        elif isinstance(item, UserKeywordController):
            self.keywords.add(item)
            item.set_parent(self.keywords)
        else:
            self.variables.add_variable(item.name, item.value, item.comment)

    def sort_keywords(self):
        if self.keywords:
            index_difference = self.keywords.sort()
            self.mark_dirty()
            RideDataFileSet(item=self).publish() #TODO: Use a more gentle message
            return index_difference
        return None

    def restore_keyword_order(self, index_difference):
        if self.keywords and index_difference:
            self.keywords.restore_keyword_order(index_difference)
            self.mark_dirty()
            RideDataFileSet(item=self).publish() #TODO: Use a more gentle message

    def get_keyword_names(self):
        if self.keywords:
            return [kw.name for kw in self.keywords._items]
        return None

    def has_format(self):
        return True

    def get_format(self):
        if not self.filename:
            return None
        return os.path.splitext(self.filename)[1].replace('.', '')

    def set_format(self, format):
        self.data.source = utils.replace_extension(self.filename, format)
        self.filename = self.data.source

    def is_same_format(self, format):
        if format and self.has_format():
            return format.lower() == self.get_format().lower()
        return False

    def set_basename(self, basename):
        old_file = self.filename
        self.data.source = os.path.join(self.directory, '%s.%s' % (basename, self.get_format()))
        self.filename = self.data.source
        self.execute(commands.SaveFile())
        if old_file != self.filename:
            self.remove_from_filesystem(old_file)

    def remove_from_filesystem(self, path=None):
        path = path or self.filename
        if os.path.exists(path):
            os.remove(path)

    def remove_folder_from_filesystem(self, path=None):
        shutil.rmtree(path or self.data.directory)

    def save_with_new_format(self, format):
        self._project.change_format(self, format)

    def save_with_new_format_recursive(self, format):
        self._project.change_format_recursive(self, format)

    def validate_keyword_name(self, name):
        return self.keywords.validate_name(name)

    def is_directory_suite(self):
        return False

    def resource_import_modified(self, path):
        return self._project.resource_import_modified(path, self.directory)

    def notify_settings_changed(self):
        RideItemSettingsChanged(item=self).publish()

    def notify_steps_changed(self):
        for test in self.tests:
            test.notify_steps_changed()

    def iter_datafiles(self):
        yield self
        for child in self.children:
            for datafile in child.iter_datafiles():
                yield datafile

    def save(self):
        self._project.save(self)

    def get_local_variables(self):
        return {}

    def is_inside_top_suite(self, res):
        return False


class TestDataDirectoryController(_DataController, _FileSystemElement, _BaseController):

    def __init__(self, data, project=None, parent=None):
        dir_ = data.directory
        dir_ = os.path.abspath(dir_) if isinstance(dir_, basestring) else dir_
        _FileSystemElement.__init__(self, self._filename(data), dir_)
        _DataController.__init__(self, data, project, parent)
        self._dir_controllers = {}

    def _filename(self, data):
        return data.initfile

    @property
    def default_dir(self):
        return self.data.source

    @property
    def display_name(self):
        return self.data.name

    @property
    def longname(self):
        if self.parent:
            return self.parent.longname + '.' + self.display_name
        return self.display_name

    @property
    def suites(self):
        return [child for child in self.children if
                    isinstance(child, TestDataDirectoryController) or
                    isinstance(child, TestCaseFileController)]

    def add_child(self, child):
        self.children.append(child)

    def contains_tests(self):
        for suite in self.suites:
            if suite.contains_tests():
                return True
        return False

    def find_controller_by_longname(self, longname, testname = None):
        return self.find_controller_by_names(longname.split("."), testname)

    def find_controller_by_names(self, names, testname):
        namestring = '.'.join(names)
        if not namestring.startswith(self.name):
            return None
        if namestring == self.name:
            return self
        for suite in self.suites:
            res = suite.find_controller_by_names(namestring[len(self.name)+1:].split('.'), testname)
            if res:
                return res

    def is_excluded(self):
        return self._project.is_excluded(self.source) if self._project else False

    def _children(self, data):
        children = [DataController(child, self._project, self) for child in data.children]
        if self._can_add_directory_children(data):
            self._add_directory_children(children, data.source, data.initfile)
        return children

    def _can_add_directory_children(self, data):
        return data.source and os.path.isdir(data.source) and self._namespace

    def _add_directory_children(self, children, path, initfile):
        for filename in self._get_unknown_files_in_directory(children, path, initfile):
            if not self._is_robot_ignored_name(filename):
                self._add_directory_child(children, filename)

    def _is_robot_ignored_name(self, filename):
        base = os.path.basename(filename)
        return any(base.startswith(c) for c in '_.')

    def _add_directory_child(self, children, filename):
        if os.path.isdir(filename):
            children.append(self._directory_controller(filename))
        else:
            r = self._namespace.get_resource(filename, report_status=False)
            if self._is_valid_resource(r):
                children.append(self._resource_controller(r))

    def _directory_controller(self, path):
        dc = TestDataDirectoryController(TestDataDirectory(source=path),
                                         project=self._project,
                                         parent=self)
        self._add_directory_children(dc.children, dc.source, None)
        return dc

    def _is_valid_resource(self, resource):
        return resource and (resource.setting_table or resource.variable_table or resource.keyword_table or os.stat(resource.source)[6]==0)

    def _resource_controller(self, resource):
        resource_control =  self._resource_file_controller_factory.create(resource)
        resource_control.parent = self
        return resource_control

    def _get_unknown_files_in_directory(self, children, path, initfile):
        already_in_use = [c.filename for c in children] + [initfile]
        already_in_use += [c.directory for c in children]
        return [f for f in self._get_filenames_in_directory(path) if f not in already_in_use]

    def _get_filenames_in_directory(self, path):
        return [os.path.join(path, f) for f in os.listdir(path)]

    def add_child(self, controller):
        assert controller not in self.children
        self.children.append(controller)

    def has_format(self):
        return self.data.initfile is not None

    def set_format(self, format):
        self.data.initfile = os.path.join(self.data.source, '__init__.%s'
                                          % format.lower())
        self.filename = self.data.initfile

    def new_test_case_file(self, path):
        ctrl = self._new_data_controller(NewTestCaseFile(path))
        ctrl.mark_dirty()
        return ctrl

    def new_test_data_directory(self, path):
        return self._new_data_controller(NewTestDataDirectory(path))

    def _new_data_controller(self, datafile):
        self.data.children.append(datafile)
        datafile.parent = self.data
        self.children.append(DataController(datafile, self._project, self))
        return self.children[-1]

    def notify_suite_added(self, suite):
        RideSuiteAdded(parent=self, suite=suite).publish()

    def is_directory_suite(self):
        return True

    def reload(self):
        self.__init__(TestDataDirectory(source=self.directory, parent=self.data.parent).populate(),
                      self._project, parent=self.parent)

    def remove(self):
        path = self.filename
        self.data.initfile = None
        self._stat = self._get_stat(None)
        self.reload()
        RideInitFileRemoved(path=path, datafile=self).publish()

    def _remove_resources(self):
        for resource in self._find_resources_recursively(self):
            self._project.remove_resource(resource)
            RideDataFileRemoved(path=resource.filename, datafile=resource).publish()

    def remove_from_model(self):
        self._project.remove_datafile(self)
        self._remove_resources()
        RideDataFileRemoved(path=self.filename, datafile=self).publish()

    def remove_child(self, controller):
        if controller in self.children:
            self.children.remove(controller)
        else:
            for child in self.children:
                child.remove_child(controller)

    def is_inside_top_suite(self, ctrl):
        return ctrl.filename.startswith(self.directory)

    def _is_inside_test_data_directory(self, directory):
        return any(True for s in [self] + self.children
                   if s.is_directory_suite() and s.directory == directory)

    def remove_static_imports_to_this(self):
        for resource_import in self.get_where_used():
            resource_import[1].remove()

    def get_where_used(self):
        for imp in self._get_recursive_imports():
            yield imp

    def _all_imports(self):
        for df in self.datafiles:
            for imp in df.imports:
                yield imp

    def _get_recursive_imports(self):
        all_imports = self._all_imports()
        ctrls = self._find_controllers_recursively(self)
        for res in self._find_resources_recursively(self):
            for imp in all_imports:
                if imp.get_imported_controller() == res and imp.parent.parent not in ctrls:
                    yield res, imp

    def _find_resources_recursively(self, controller):
        resources = []
        if controller.children:
            for child in controller.children:
                resources += self._find_resources_recursively(child)
        elif isinstance(controller, ResourceFileController):
            resources.append(controller)
        return resources

    def _find_controllers_recursively(self, controller):
        resources = []
        if controller.children:
            for child in controller.children:
                resources += self._find_controllers_recursively(child)
        resources.append(controller)
        return resources

    def insert_to_test_data_directory(self, res):
        res_dir = os.path.dirname(res.filename)
        if res_dir in self._dir_controllers:
            self._dir_controllers[res_dir].add_child(res)
        else:
            target = self._find_closest_directory(res)
            if target is self:
                self._create_target_dir_controller(res, res_dir, target)
            else:
                target.insert_to_test_data_directory(res)

    def _find_closest_directory(self, res):
        target = self
        for s in self.iter_datafiles():
            if not isinstance(s, TestDataDirectoryController):
                continue
            if res.filename.startswith(s.directory):
                target = s
        return target

    def _create_target_dir_controller(self, res, res_dir, target):
        for dirname in res_dir[len(self.directory):].split(os.sep):
            if not dirname:
                continue
            target_dir = os.path.join(target.directory, dirname)
            dir_ctrl = TestDataDirectoryController(TestDataDirectory(source=target_dir), self._project, self)
            target._dir_controllers[target.directory] = dir_ctrl
            target.add_child(dir_ctrl)
            if target_dir == res_dir:
                dir_ctrl.add_child(res)
                return
            target = dir_ctrl
        if res not in self.children:
            self.add_child(res)

    def new_resource(self, path):
        ctrl = self._project.new_resource(path, parent=self)
        ctrl.mark_dirty()
        return ctrl

    def exclude(self):
        if self._project.is_datafile_dirty(self):
            raise DirtyRobotDataException()
        self._project._settings.excludes.update_excludes([self.directory])
        index = self.parent.children.index(self)
        result = ExcludedDirectoryController(self.data, self._project, self.parent)
        self.parent.children[index] = result
        return result


class DirtyRobotDataException(Exception):
    """
    Raised when data is dirty and you are trying to do an operation that requires undirty data.
    """
    pass


class TestCaseFileController(_FileSystemElement, _DataController):

    def __init__(self, data, project=None, parent=None):
        _FileSystemElement.__init__(self, data.source if data else None, data.directory)
        _DataController.__init__(self, data, project, parent)

    def _settings(self):
        ss = self._setting_table
        sett = _DataController._settings(self)
        sett.insert(-1, TemplateController(self, ss.test_template))
        sett.insert(-1, TimeoutController(self, ss.test_timeout))
        return sett + [self.default_tags]

    @property
    def longname(self):
        if self.parent:
            return self.parent.longname + '.' + self.name
        return self.name

    @property
    def suites(self):
        return ()

    def contains_tests(self):
        return bool(self.tests)

    def find_controller_by_longname(self, longname, node_testname = None):
        return self.find_controller_by_names(longname.split("."), node_testname)


    def find_controller_by_names(self, names, node_testname = None):
        names = '.'.join(names)
        if not names.startswith(self.name):
            return None
        if len(self.name) < len(names) and not names.startswith(self.name+'.'):
            return None
        if len(names) == 1:
            return self
        for test in self.tests:
            if test.name == node_testname:
                return test
        return None

    @property
    def default_tags(self):
        return DefaultTagsController(self, self._setting_table.default_tags)

    def is_modifiable(self):
        return not self.exists() or not self.is_readonly()

    def create_test(self, name):
        return self.tests.new(name)

    def validate_test_name(self, name):
        return self.tests.validate_name(name)

    def remove_child(self, controller):
        if controller is self:
            self.remove()

    def remove(self):
        self._project.remove_datafile(self)
        RideDataFileRemoved(path=self.filename, datafile=self).publish()

    def reload(self):
        self.__init__(TestCaseFile(parent=self.data.parent, source=self.filename).populate(),
                      project=self._project,
                      parent=self.parent)

    def get_template(self):
        return self.data.setting_table.test_template


class ResourceFileControllerFactory(object):

    def __init__(self, namespace, project):
        self._resources = []
        self._namespace = namespace
        self._project = project
        self._all_resource_imports_resolved = False

    @property
    def resources(self):
        return self._resources

    def find(self, data):
        return self._find_with_source(data.source)

    def _find_with_source(self, source):
        for other in self.resources:
            if other.filename == source:
                return other
        return None

    def find_with_import(self, import_):
        resource_model = self._namespace.find_resource_with_import(import_)
        if not resource_model:
            return None
        res = self.find(resource_model)
        if not res:
            res = self.create(resource_model)
            self._project.insert_into_suite_structure(res)
        assert(res is not None)
        return res

    def create(self, data, parent=None):
        rfc = ResourceFileController(data, self._project, parent, self)
        self.resources.append(rfc)
        self.set_all_resource_imports_unresolved()
        return rfc

    def set_all_resource_imports_resolved(self):
        self._all_resource_imports_resolved = True

    def set_all_resource_imports_unresolved(self):
        self._all_resource_imports_resolved = False

    def is_all_resource_file_imports_resolved(self):
        return self._all_resource_imports_resolved

    def remove(self, controller):
        self._resources.remove(controller)
        self.set_all_resource_imports_unresolved()


class ResourceFileController(_FileSystemElement, _DataController):

    def __init__(self, data, project=None, parent=None, resource_file_controller_factory=None):
        self._resource_file_controller_factory = resource_file_controller_factory
        self._known_imports = set()
        _FileSystemElement.__init__(self, data.source if data else None, data.directory)
        _DataController.__init__(self, data, project,
                                 parent or self._find_parent_for(project, data.source))
        if self.parent and self not in self.parent.children:
            self.parent.add_child(self)
        self._unresolve_all_if_none_existing()

    def _unresolve_all_if_none_existing(self):
        if not self.exists() and self._resource_file_controller_factory:
            self._resource_file_controller_factory.set_all_resource_imports_unresolved() # Some import may have referred to this none existing resource

    def _find_parent_for(self, project, source):
        if not project:
            return None
        dir = os.path.dirname(source)
        for ctrl in project.datafiles:
            if ctrl.is_directory_suite() and self._to_os_style(ctrl.directory) == dir:
                return ctrl
        return None

    def _to_os_style(self, path):
        return path.replace('/', os.sep)

    @property
    def display_name(self):
        _, tail = os.path.split(self.data.source)
        return tail

    def is_modifiable(self):
        return not self.exists() or not self.is_readonly()

    def set_format(self, format):
        self._modify_file_name(lambda: _DataController.set_format(self, format),
                               lambda imp: imp.change_format(format))

    def set_basename(self, basename):
        self._modify_file_name(lambda: _DataController.set_basename(self, basename),
                               lambda imp: imp.unresolve())

    def set_basename_and_modify_imports(self, basename):
        old = self.filename
        self._modify_file_name(lambda: _DataController.set_basename(self, basename),
                               lambda imp: imp.change_name(os.path.basename(old), os.path.basename(self.filename)))

    def remove_static_imports_to_this(self):
        name = os.path.basename(self.filename)
        # have to resolve imports before deleting
        # see: http://code.google.com/p/robotframework-ride/issues/detail?id=1119
        imports = [import_ for import_ in self.get_where_used()]
        for resource_import in imports:
            if resource_import.name.endswith(name):
                resource_import.remove()

    def _modify_file_name(self, modification, notification):
        old = self.filename
        modification()
        resource_imports = [resource_import_ for resource_import_ in self.get_where_used()]
        for resource_import in resource_imports:
            notification(resource_import)
        self._namespace.resource_filename_changed(old, self.filename)

    def _settings(self):
        return [DocumentationController(self, self.data.setting_table.doc)]

    def validate_name(self, name):
        for uk in self.data.keyword_table.keywords:
            if uk != name and utils.eq(uk.name, name):
                return 'User keyword with this name already exists.'
        return None

    def reload(self):
        self.__init__(ResourceFile(source=self.filename).populate(), self._project, parent=self.parent)

    def remove(self):
        self._project.remove_resource(self)
        RideDataFileRemoved(path=self.filename, datafile=self).publish()

    def remove_known_import(self, _import):
        self._known_imports.remove(_import)

    def add_known_import(self, _import):
        self._known_imports.add(_import)

    def notify_opened(self):
        RideOpenResource(path=self.filename, datafile=self).publish()
        for _import in [ imp for imp in self.imports if imp.is_resource ]:
            _import.import_loaded_or_modified()

    def is_used(self):
        if self._known_imports:
            return True
        if self._resource_file_controller_factory.is_all_resource_file_imports_resolved():
            return False
        return any(self._resolve_known_imports())

    def get_where_used(self):
        if self._resource_file_controller_factory.is_all_resource_file_imports_resolved():
            source = self._known_imports
        else:
            source = self._resolve_known_imports()

        for usage in source:
            yield usage


    def _resolve_known_imports(self):
        for imp in self._all_imports():
            if imp.get_imported_controller() is self:
                yield imp
        self._resource_file_controller_factory.set_all_resource_imports_resolved()

    def _all_imports(self):
        for df in self.datafiles:
            for imp in df.imports:
                yield imp

    def remove_child(self, controller):
        pass

class ExcludedDirectoryController(_FileSystemElement, ControllerWithParent, WithNamespace):

    def __init__(self, data, project, parent):
        self.data = data
        self._project = project
        if self._project:
            self._set_namespace_from(self._project)
            self._resource_file_controller_factory =\
            self._project.resource_file_controller_factory
        else:
            self._resource_file_controller_factory = None
        self._parent = parent
        self.children = []
        self.keywords = []
        self.variables = tuple()
        self.tests = tuple()
        self.imports = tuple()
        _FileSystemElement.__init__(self, '', data.directory)

    @property
    def settings(self):
        return self._settings()

    def _settings(self):
        ss = self.data.setting_table
        return [DocumentationController(self, ss.doc),
                FixtureController(self, ss.suite_setup),
                FixtureController(self, ss.suite_teardown),
                FixtureController(self, ss.test_setup),
                FixtureController(self, ss.test_teardown),
                self.force_tags]

    @property
    def _setting_table(self):
        return self.data.setting_table

    @property
    def force_tags(self):
        return ForceTagsController(self, self._setting_table.force_tags)

    @property
    def dirty(self):
        return False

    def keyword_info(self, keyword_name):
        return WithNamespace.keyword_info(self, self.data, keyword_name)

    @overrides(_BaseController)
    def is_excluded(self):
        return True

    def remove_from_excludes(self):
        self._project._settings.excludes.remove_path(self.source)
        index = self.parent.children.index(self)
        td = TestData(self.data.source, self.parent.data, self._project._settings)
        result = TestDataDirectoryController(td, self._project, self.parent)
        self.parent.children[index] = result
        return result

    def iter_datafiles(self):
        return [self]

    @property
    def name(self):
        return self.data.name

    def is_directory_suite(self):
        return True

    def add_child(self, child):
        self.children.append(child)
