#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Mon Feb 14 17:54:58 2022

@author: nicolas
"""
from glob import glob
from os.path import expanduser
from pathlib import Path
from typing import (
    Dict,
    List,
    Iterable,
    Type,
    Union,
    Optional,
)

from websites_test_framework.test_class import WebsiteTest
from websites_test_framework.custom_types import TestInfo, TestName, TestsResults, Scores, PathLike
from websites_test_framework.param import LOG_DIR_NAME
from websites_test_framework.tools import (
    list2xlsx,
    cached_property,
    as_path,
    warn,
)


# def between_0_and_1(x: float):
#     """Return 0 if x <= 0 and 1 if x >= 1."""
#     return max(0.0, min(1.0, x))


class CollectTestsResults:
    def __init__(
        self,
        paths: Iterable[PathLike],
        test_class: Type[WebsiteTest] = WebsiteTest,
        path_on_server: str = "/",
    ):
        # Sort paths for so that order is deterministic (easier to debug).
        self.paths = sorted(as_path(path) for path in paths)
        self.test_class: Type[WebsiteTest] = test_class
        self._path_on_server = path_on_server

    @cached_property
    def _testers(self) -> Dict[Path, WebsiteTest]:
        return {
            path: self.test_class(path, path_on_server=self._path_on_server) for path in self.paths
        }

    @cached_property
    def results(self) -> Dict[Path, TestsResults]:
        return {path: tester.run() for path, tester in self._testers.items()}

    @cached_property
    def _raw_scores(self) -> Scores:
        return {
            path: {name: result["score"] for name, result in self.results[path].items()}
            for path in self.paths
        }

    @cached_property
    def scores(self) -> Scores:
        """Return the scores for each test for all websites.

        For each test marked as relative, scores are divided by the best score of all websites.
        """
        scores: Scores = {}
        for path, results in self._raw_scores.items():
            scores[path] = {}
            for test_name, score in results.items():
                scores[path][test_name] = score
                if (
                    getattr(self.test_class, test_name).is_relative
                    and (best := self.best_scores[test_name]) != 0
                ):
                    # Make score relative to best score.
                    scores[path][test_name] /= best
        return scores

    @cached_property
    def best_scores(self) -> Dict[TestName, Optional[float]]:
        best_scores = {}
        for test_name in self.test_class.get_all_tests_names():
            best_scores[test_name] = max(
                (self._raw_scores[path][test_name] for path in self.paths), default=None
            )
        return best_scores

    @cached_property
    def logs(self) -> Dict[Path, Dict[TestName, str]]:
        return {
            path: {name: result["log"] for name, result in self.results[path].items()}
            for path in self.paths
        }

    @cached_property
    def global_scores(self) -> Dict[Path, float]:
        """Return the global score of each website.

        The global score is the weighted arithmetic mean of all tests scores.
        """
        total = sum(self.weights.values())
        weights = list(self.weights.values())

        def _scores(path):
            return self.scores[path].values()

        if total == 0:
            raise RuntimeError(f"No test found ! ({self.test_class.tests_names})")
        return {
            path: sum(weight * score for weight, score in zip(weights, _scores(path))) / total
            for path in self.results
        }

    @cached_property
    def authors(self) -> Dict[Path, List[str]]:
        try:
            return {path: tester.get_authors() for path, tester in self._testers.items()}
        except NotImplementedError:
            if self.test_class != WebsiteTest:
                warn(
                    "No `.get_authors()` method found",
                    f"you should overwrite it in your class `{self.test_class.__qualname__}`.",
                )
            return {path: [f"{i}-{path.name}"] for i, path in enumerate(self.paths)}

    @cached_property
    def _tests_infos(self) -> Dict[TestName, TestInfo]:
        return self.test_class.get_tests_infos()

    @cached_property
    def titles(self) -> Dict[TestName, str]:
        return {name: infos["title"] for name, infos in self._tests_infos.items()}

    @cached_property
    def weights(self) -> Dict[TestName, float]:
        return {name: infos["weight"] for name, infos in self._tests_infos.items()}

    def write_log(self, folder: PathLike = "."):
        folder = as_path(folder) / LOG_DIR_NAME
        folder.mkdir(exist_ok=True)
        for path in self.paths:
            full_log: List[str] = [f"# Website: {path}"]
            scores = self.scores[path]
            logs = self.logs[path]
            for test_name in self.results[path]:
                title = self.titles[test_name]
                full_log.append(f"\n## {title}")
                full_log.append(f"score: {scores[test_name]}")
                full_log.append(f"weight: {self.weights[test_name]}")
                full_log.append(logs[test_name])
            log_text = "\n".join(full_log)
            for name in self.authors[path]:
                (folder / f"{name}.md").write_text(log_text, encoding="utf8")

    def write_xlsx_file(self, xlsx_path: PathLike = "scores.xlsx"):
        """Write all tests results inside an XSLX file."""
        # First line: test title
        data: List[List[Union[str, float]]] = [
            ["Name", "Score", *(title for title in self.titles.values())],
            ["", "", *self.weights.values()],
        ]
        # Second line: test weight
        for path in self.paths:
            # First column: name ; second column: global score
            row = ["", round(20 * self.global_scores[path], 2), *self.scores[path].values()]
            for name in self.authors[path]:
                row[0] = name
                data.append(row[:])  # make a copy of the list !
        Path(xlsx_path).unlink(missing_ok=True)
        list2xlsx(data, xlsx_path)
        print(f"\n\033[1;32mXLSX file generated:\033[0m {xlsx_path}.")


def run_and_collect(
    test_class: Type[WebsiteTest] = WebsiteTest,
    input_dir_or_glob: Union[PathLike, Iterable[Path]] = ".",
    output_file_or_dir: PathLike = ".",
    path_on_server: str = "/",
):
    """Run all tests, write log files and generate XLSX file with results.

    `input_dir_or_glob` is either:
     - the name of the parent folder containing all the websites' root directories,
     - a glob like '/home/*/*/www',
     - a list of paths, each path corresponding to the root directory of a website.

    `output_file_or_dir` is either a directory or the name of a XLSX file.

    `path_on_server` is used to rewrite absolute links, when server root is not website root.
    For example, if the website folder is /path/to/website on the server, path_on_server
    should be "/path/to/website".
    """
    print("\nStarting tests...\n")
    if isinstance(input_dir_or_glob, str) and "*" in input_dir_or_glob:
        # input_dir_or_glob is a glob
        paths: Iterable[Path] = (as_path(pth) for pth in glob(expanduser(input_dir_or_glob)))
    elif isinstance(input_dir_or_glob, (str, Path)):
        # input_dir_or_glob is a directory path
        input_dir_or_glob = as_path(input_dir_or_glob)
        if not input_dir_or_glob.is_dir():
            raise FileNotFoundError(f"Directory not found: {input_dir_or_glob!r}")
        paths = (pth for pth in input_dir_or_glob.glob("*") if pth.is_dir())
    else:
        paths = (as_path(pth) for pth in input_dir_or_glob)

    output_file_or_dir = as_path(output_file_or_dir)
    if output_file_or_dir.suffix == ".xlsx":
        output_dir = output_file_or_dir.parent
        output_file = output_file_or_dir
    else:
        output_dir = output_file_or_dir
        output_file = output_file_or_dir / "scores.xlsx"
    # Exclude LOG_DIR_NAME from tested paths, in case run_and_collect()
    # is called twice and input and output are set
    # to current directory (which is their default value).
    collector = CollectTestsResults(
        (pth for pth in paths if pth.name != LOG_DIR_NAME), test_class, path_on_server
    )
    output_dir.mkdir(exist_ok=True)
    collector.write_log(output_dir)
    collector.write_xlsx_file(output_file)


if __name__ == "__main__":
    run_and_collect()
