#!/usr/bin/env python
#
# Joint copyright:
#  - Copyright 2012,2013 Wikimedia Foundation
#  - Copyright 2012,2013 Antoine "hashar" Musso
#  - Copyright 2013 Arnaud Fabre
#
# 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 doctest
import configparser
import io
import json
import logging
import os
import pkg_resources
import re
import xml.etree.ElementTree as XML

import fixtures
import six
from six.moves import StringIO
import testtools
from testtools.content import text_content
import testscenarios
from yaml import safe_dump

from jenkins_jobs.config import JJBConfig
from jenkins_jobs.errors import InvalidAttributeError
import jenkins_jobs.local_yaml as yaml
from jenkins_jobs.alphanum import AlphanumSort
from jenkins_jobs.modules import project_externaljob
from jenkins_jobs.modules import project_flow
from jenkins_jobs.modules import project_matrix
from jenkins_jobs.modules import project_maven
from jenkins_jobs.modules import project_multibranch
from jenkins_jobs.modules import project_multijob
from jenkins_jobs.modules import view_all
from jenkins_jobs.modules import view_delivery_pipeline
from jenkins_jobs.modules import view_list
from jenkins_jobs.modules import view_nested
from jenkins_jobs.modules import view_pipeline
from jenkins_jobs.modules import view_sectioned
from jenkins_jobs.parser import YamlParser
from jenkins_jobs.registry import ModuleRegistry
from jenkins_jobs.xml_config import XmlJob
from jenkins_jobs.xml_config import XmlJobGenerator

# This dance deals with the fact that we want unittest.mock if
# we're on Python 3.4 and later, and non-stdlib mock otherwise.
try:
    from unittest import mock  # noqa
except ImportError:
    import mock  # noqa


def get_scenarios(
    fixtures_path,
    in_ext="yaml",
    out_ext="xml",
    plugins_info_ext="plugins_info.yaml",
    filter_func=None,
):
    """Returns a list of scenarios, each scenario being described
    by two parameters (yaml and xml filenames by default).
        - content of the fixture output file (aka expected)
    """
    scenarios = []
    files = {}
    for dirpath, _, fs in os.walk(fixtures_path):
        for fn in fs:
            if fn in files:
                files[fn].append(os.path.join(dirpath, fn))
            else:
                files[fn] = [os.path.join(dirpath, fn)]

    input_files = [
        files[f][0] for f in files if re.match(r".*\.{0}$".format(in_ext), f)
    ]

    for input_filename in input_files:
        if input_filename.endswith(plugins_info_ext):
            continue

        if callable(filter_func) and filter_func(input_filename):
            continue

        output_candidate = re.sub(
            r"\.{0}$".format(in_ext), ".{0}".format(out_ext), input_filename
        )
        # assume empty file if no output candidate found
        if os.path.basename(output_candidate) in files:
            out_filenames = files[os.path.basename(output_candidate)]
        else:
            out_filenames = None

        plugins_info_candidate = re.sub(
            r"\.{0}$".format(in_ext), ".{0}".format(plugins_info_ext), input_filename
        )
        if os.path.basename(plugins_info_candidate) not in files:
            plugins_info_candidate = None

        conf_candidate = re.sub(r"\.yaml$|\.json$", ".conf", input_filename)
        conf_filename = files.get(os.path.basename(conf_candidate), None)

        if conf_filename:
            conf_filename = conf_filename[0]
        else:
            # for testing purposes we want to avoid using user config files
            conf_filename = os.devnull

        scenarios.append(
            (
                input_filename,
                {
                    "in_filename": input_filename,
                    "out_filenames": out_filenames,
                    "conf_filename": conf_filename,
                    "plugins_info_filename": plugins_info_candidate,
                },
            )
        )

    return scenarios


class BaseTestCase(testtools.TestCase):

    # TestCase settings:
    maxDiff = None  # always dump text difference
    longMessage = True  # keep normal error message when providing our

    def setUp(self):

        super(BaseTestCase, self).setUp()
        self.logger = self.useFixture(fixtures.FakeLogger(level=logging.DEBUG))

    def _read_utf8_content(self):
        # if None assume empty file
        if not self.out_filenames:
            return u""

        # Read XML content, assuming it is unicode encoded
        xml_content = ""
        for f in sorted(self.out_filenames):
            with io.open(f, "r", encoding="utf-8") as xml_file:
                xml_content += u"%s" % xml_file.read()
        return xml_content

    def _read_yaml_content(self, filename):
        with io.open(filename, "r", encoding="utf-8") as yaml_file:
            yaml_content = yaml.load(yaml_file)
        return yaml_content

    def _get_config(self):
        jjb_config = JJBConfig(self.conf_filename)
        jjb_config.validate()

        return jjb_config


class BaseScenariosTestCase(testscenarios.TestWithScenarios, BaseTestCase):

    scenarios = []
    fixtures_path = None

    @mock.patch("pkg_resources.iter_entry_points")
    def test_yaml_snippet(self, mock):
        if not self.in_filename:
            return

        jjb_config = self._get_config()

        expected_xml = self._read_utf8_content()
        yaml_content = self._read_yaml_content(self.in_filename)

        plugins_info = None
        if self.plugins_info_filename:
            plugins_info = self._read_yaml_content(self.plugins_info_filename)
            self.addDetail(
                "plugins-info-filename", text_content(self.plugins_info_filename)
            )
            self.addDetail("plugins-info", text_content(str(plugins_info)))

        parser = YamlParser(jjb_config)
        e = pkg_resources.EntryPoint.parse
        d = pkg_resources.Distribution()
        config = configparser.ConfigParser()
        config.read(os.path.dirname(__file__) + "/../setup.cfg")
        groups = {}
        for key in config["entry_points"]:
            groups[key] = list()
            for line in config["entry_points"][key].split("\n"):
                if "" == line.strip():
                    continue
                groups[key].append(e(line, dist=d))

        def mock_iter_entry_points(group, name=None):
            return (
                entry for entry in groups[group] if name is None or name == entry.name
            )

        mock.side_effect = mock_iter_entry_points
        registry = ModuleRegistry(jjb_config, plugins_info)
        registry.set_parser_data(parser.data)

        pub = self.klass(registry)

        project = None
        if "project-type" in yaml_content:
            if yaml_content["project-type"] == "maven":
                project = project_maven.Maven(registry)
            elif yaml_content["project-type"] == "matrix":
                project = project_matrix.Matrix(registry)
            elif yaml_content["project-type"] == "flow":
                project = project_flow.Flow(registry)
            elif yaml_content["project-type"] == "multijob":
                project = project_multijob.MultiJob(registry)
            elif yaml_content["project-type"] == "multibranch":
                project = project_multibranch.WorkflowMultiBranch(registry)
            elif yaml_content["project-type"] == "multibranch-defaults":
                project = project_multibranch.WorkflowMultiBranchDefaults(
                    registry
                )  # noqa
            elif yaml_content["project-type"] == "externaljob":
                project = project_externaljob.ExternalJob(registry)

        if "view-type" in yaml_content:
            if yaml_content["view-type"] == "all":
                project = view_all.All(registry)
            elif yaml_content["view-type"] == "delivery_pipeline":
                project = view_delivery_pipeline.DeliveryPipeline(registry)
            elif yaml_content["view-type"] == "list":
                project = view_list.List(registry)
            elif yaml_content["view-type"] == "nested":
                project = view_nested.Nested(registry)
            elif yaml_content["view-type"] == "pipeline":
                project = view_pipeline.Pipeline(registry)
            elif yaml_content["view-type"] == "sectioned":
                project = view_sectioned.Sectioned(registry)
            else:
                raise InvalidAttributeError("view-type", yaml_content["view-type"])

        if project:
            xml_project = project.root_xml(yaml_content)
        else:
            xml_project = XML.Element("project")

        # Generate the XML tree directly with modules/general
        pub.gen_xml(xml_project, yaml_content)

        # check output file is under correct path
        if "name" in yaml_content:
            prefix = os.path.dirname(self.in_filename)
            # split using '/' since fullname uses URL path separator
            expected_folders = [
                os.path.normpath(
                    os.path.join(
                        prefix,
                        "/".join(parser._getfullname(yaml_content).split("/")[:-1]),
                    )
                )
            ]
            actual_folders = [os.path.dirname(f) for f in self.out_filenames]

            self.assertEquals(
                expected_folders,
                actual_folders,
                "Output file under wrong path, was '%s', should be '%s'"
                % (
                    self.out_filenames[0],
                    os.path.join(
                        expected_folders[0], os.path.basename(self.out_filenames[0])
                    ),
                ),
            )

        # Prettify generated XML
        pretty_xml = XmlJob(xml_project, "fixturejob").output().decode("utf-8")

        self.assertThat(
            pretty_xml,
            testtools.matchers.DocTestMatches(
                expected_xml, doctest.ELLIPSIS | doctest.REPORT_NDIFF
            ),
        )


class SingleJobTestCase(BaseScenariosTestCase):
    def test_yaml_snippet(self):
        config = self._get_config()

        expected_xml = (
            self._read_utf8_content()
            .strip()
            .replace("<BLANKLINE>", "")
            .replace("\n\n", "\n")
        )

        parser = YamlParser(config)
        parser.parse(self.in_filename)

        plugins_info = None
        if self.plugins_info_filename:
            plugins_info = self._read_yaml_content(self.plugins_info_filename)
            self.addDetail(
                "plugins-info-filename", text_content(self.plugins_info_filename)
            )
            self.addDetail("plugins-info", text_content(str(plugins_info)))

        registry = ModuleRegistry(config, plugins_info)
        registry.set_parser_data(parser.data)
        job_data_list, view_data_list = parser.expandYaml(registry)

        # Generate the XML tree
        xml_generator = XmlJobGenerator(registry)
        xml_jobs = xml_generator.generateXML(job_data_list)

        xml_jobs.sort(key=AlphanumSort)

        # check reference files are under correct path for folders
        prefix = os.path.dirname(self.in_filename)
        # split using '/' since fullname uses URL path separator
        expected_folders = list(
            set(
                [
                    os.path.normpath(
                        os.path.join(prefix, "/".join(job_data["name"].split("/")[:-1]))
                    )
                    for job_data in job_data_list
                ]
            )
        )
        actual_folders = [os.path.dirname(f) for f in self.out_filenames]

        six.assertCountEqual(
            self,
            expected_folders,
            actual_folders,
            "Output file under wrong path, was '%s', should be '%s'"
            % (
                self.out_filenames[0],
                os.path.join(
                    expected_folders[0], os.path.basename(self.out_filenames[0])
                ),
            ),
        )

        # Prettify generated XML
        pretty_xml = (
            u"\n".join(job.output().decode("utf-8") for job in xml_jobs)
            .strip()
            .replace("\n\n", "\n")
        )

        self.assertThat(
            pretty_xml,
            testtools.matchers.DocTestMatches(
                expected_xml, doctest.ELLIPSIS | doctest.REPORT_NDIFF
            ),
        )


class JsonTestCase(BaseScenariosTestCase):
    def test_yaml_snippet(self):
        expected_json = self._read_utf8_content()
        yaml_content = self._read_yaml_content(self.in_filename)

        pretty_json = json.dumps(yaml_content, indent=4, separators=(",", ": "))

        self.assertThat(
            pretty_json,
            testtools.matchers.DocTestMatches(
                expected_json, doctest.ELLIPSIS | doctest.REPORT_NDIFF
            ),
        )


class YamlTestCase(BaseScenariosTestCase):
    def test_yaml_snippet(self):
        expected_yaml = self._read_utf8_content()
        yaml_content = self._read_yaml_content(self.in_filename)

        # using json forces expansion of yaml anchors and aliases in the
        # outputted yaml, otherwise it would simply appear exactly as
        # entered which doesn't show that the net effect of the yaml
        data = StringIO(json.dumps(yaml_content))

        pretty_yaml = safe_dump(json.load(data), default_flow_style=False)

        self.assertThat(
            pretty_yaml,
            testtools.matchers.DocTestMatches(
                expected_yaml, doctest.ELLIPSIS | doctest.REPORT_NDIFF
            ),
        )
