#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Dépend de websites_test_framework.
"""
import csv
import re
from html import unescape
from types import TracebackType
from typing import List, Type, Optional, Tuple, Dict

from websites_test_framework import WebsiteTest, run_and_collect, test
from websites_test_framework.css_parser import (
    CssError,
    CssSimpleParser,
    looks_like_css_code,
)
from websites_test_framework.tools import detect_encoding
from websites_test_framework.website import HTMLWebFile, CSSWebFile

WEBSITES_PATH = "/home/nicolas/R102/rendus/*/www/R102"
OUTPUT_DIR = "/home/nicolas/Travail/enseignements/S1/R102 - developpement web/rendus"
PATH_ON_SERVER = "/R102"  # to rewrite absolute URLS. Server root is www/, not www/R102/

# Liste des étudiants téléchargeable depuis :
# https://planier.unice.fr/listes-sesame/csv/TBFCT1-210.csv
# Mais ce fichier est mal encodé (différents encodages dans le même fichier !)
# LibreOffice arrive à l'ouvrir, il suffit de supprimer la 1re ligne est le réenregistrer.
STUDENTS_LOGINS = "/home/nicolas/R102/rendus/TBFCT1-210.csv"


def get_logins() -> Dict[str, str]:
    logins: Dict[str, str] = {}
    encoding = detect_encoding(STUDENTS_LOGINS)
    if encoding == "binary":
        raise RuntimeError(
            f"File {STUDENTS_LOGINS} isn't correctly encoded"
            "(not utf8 nor latin1 nor any known encoding !)\n"
            "Try to open it with libreoffice, remove wrong characters, "
            "save it and then relaunch this script."
        )
    with open(STUDENTS_LOGINS, encoding=encoding) as f:
        for row in csv.reader(f):
            name, surname, login, *_ = row
            if name != "nom":
                logins[login] = f"{name} {surname}"
    return logins


LOGINS_DICT = get_logins()


def child_names(element):
    return [child.name for child in element.children if child.name]


def remove_text_between_tags(s):
    tag = False
    content = []
    for char in s:
        if char == "<":
            tag = True
            content.append(char)
        elif char == ">":
            if tag:
                tag = False
            content.append(char)
        else:
            if tag:
                content.append(char)
    return "".join(content)


class Score:
    def __init__(self, *, max, value: float = 0):
        self.max = max
        self.value = value
        self._log: List[str] = []

    def __iadd__(self, value: float):
        self.value += value
        self._test()
        return self

    def __isub__(self, value: float):
        self.value -= value
        self._test()
        return self

    def __eq__(self, other: object):
        return self.value == other

    def __lt__(self, other: float):
        return self.value < other

    def __le__(self, other: float):
        return self.value <= other

    def __gt__(self, other: float):
        return self.value > other

    def __ge__(self, other: float):
        return self.value >= other

    def _test(self):
        if self.value > self.max:
            raise RuntimeError(f"Score.maximal should be at least {self.value}.")

    def log(self, msg: str):
        self._log.append(msg)

    @property
    def ratio_and_log(self) -> Tuple[float, List[str]]:
        return self.value / self.max, self._log

    def __str__(self):
        return f"Score({self.value}/{self.max})"


class CatchExceptions:
    def __init__(self, score: Score, *exceptions: Type[BaseException]):
        self.log = score.log
        self.exceptions = exceptions

    def __enter__(self) -> None:
        return

    def __exit__(
        self,
        exc_type: Optional[Type[BaseException]],
        exc_value: Optional[BaseException],
        traceback: Optional[TracebackType],
    ) -> Optional[bool]:
        if exc_type is not None and issubclass(exc_type, self.exceptions):
            print(msg := str(exc_value))
            self.log(msg)
            return True  # Intercept error
        return None  # for mypy


class R102WebsiteTest(WebsiteTest):
    def get_authors(self):
        login = self.path.parent.parent.name
        assert login in LOGINS_DICT, login
        return [LOGINS_DICT[login]]

    # ---------
    #    TD0
    # ---------

    @test(title="Files hierarchy", weight=1)
    def test_td0(self):
        score = Score(max=24)
        for folder in ("TD1", "TD2", "TD3", "TD4", "TD5"):
            td_path = self.path / folder
            if td_path.is_dir():
                score += 1
            if (td_path / "index.html").is_dir():
                score += 1
            if (td_path / "img").is_dir():
                score += 1
            if (td_path / "css").is_dir():
                score += 1
        if (self.path / "css").is_dir():
            score += 1
        if (self.path / "img").is_dir():
            score += 1
        if (self.path / "index.html").is_dir():
            score += 2
        return score.ratio_and_log

    # ---------
    #    TD1
    # ---------

    @test(title="HTML entities", weight=1)
    def test_html_entities(self):
        score = Score(max=60)
        with CatchExceptions(score, FileNotFoundError, AttributeError):
            a_propos = self.website["a_propos.html"]
            td1 = self.website["TD1/index.html"]
            score += 10
            mail = "[A-Za-z0-9._-]+(&commat;|&#x00040;|&#64;)[A-Za-z0-9._-]+.[A-Za-z]+"
            href1 = f'href[ ]*=[ ]*"[ ]*mailto[ ]*:[ ]*{mail}[ ]*"'
            href2 = f"href[ ]*=[ ]*'[ ]*mailto[ ]*:[ ]*{mail}[ ]*'"
            if re.search(href1, a_propos.text) or re.search(href2, a_propos.text):
                score += 10
            assert score <= 20, score
            for file in (a_propos, td1):
                bad = 0
                good = 0
                for symbol in ":!?!":
                    if " " + symbol in file.text:
                        bad += 1
                    if "&nbsp;" + symbol in file.text:
                        good += 1
                if good > 0:
                    score += max(good, 10)
                    if bad == 0:
                        score += 10
                assert score <= 60, score
        return score.ratio_and_log

    @test(title="A propos", weight=0.75)
    def test_a_propos_file(self):
        score = Score(max=20)
        with CatchExceptions(score, FileNotFoundError, AttributeError):
            a_propos = self.website["a_propos.html"]
            score += 1
            head = a_propos.structure.html.head
            body = a_propos.structure.html.body

            # Test of h1.
            if body.h1 is not None and body.h1.text.strip():
                score += 2
            # Test of title
            if head.title is not None and head.title.text.strip():
                score += 1

            # Test of <img/>
            for img in body.find_all("img"):
                score += 2
                if img.attrs.get("alt", "").strip():
                    score += 3
                src = img.attrs["src"]
                if src.startswith("img/"):
                    score += 2
                break

            # Test of ul/li.
            if body.find("ul"):
                score += 2
            score += min(len(body.find_all("li")), 5)

            # Test of email.
            if body.find_all("a", href=re.compile("^[ ]*mailto[ ]*:")):
                score += 2
            # Test of name.
            # text = body.text.lower()
            # if any(name in text for name in extract_student_name(dirname).lower().split()):
            #     score += 1
        return score.ratio_and_log

    @test(title="index.html (1)", weight=0.5)
    def test_root_index_file(self):
        score = Score(max=7)
        with CatchExceptions(score, FileNotFoundError, AttributeError):
            index = self.website["index.html"]
            score += 1
            _ = index.structure.head
            body = index.structure.html.body

            # Link to TD1/index.html
            if body.find_all("a", href="TD1/index.html"):
                score += 3

            # Link to a_propos.html
            if body.find_all("a", href="a_propos.html"):
                score += 3
        return score.ratio_and_log

    @test(title="TD1/index.html", weight=1)
    def test_td1_index_file(self):
        score = Score(max=54)
        with CatchExceptions(score, FileNotFoundError, AttributeError):
            td1 = self.website["TD1/index.html"]
            _ = td1.structure.html.head
            score += 1
            body = td1.structure.html.body
            score += 1
            # BODY STRUCTURE
            headers = body.find_all("header")
            if len(headers) == 1:
                (header,) = headers
            else:
                header = None

            mains = body.find_all("main")
            if len(mains) == 1:
                (main_,) = mains
            else:
                main_ = None

            footers = body.find_all("footer")
            if len(footers) == 1:
                (footer,) = footers
            else:
                footer = None

            if child_names(body) == ["header", "main", "footer"]:
                score += 5
            assert score <= 7

            # Test of header:
            if header:
                score += 1
                # contient h1 ?
                if header.find("h1"):
                    score += 2
            assert score <= 10

            # Test of footer:
            if footer:
                score += 1
                # footer should not be empty
                text = footer.text.lower().strip()
                if text:
                    score += 2
                # if str(datetime.now().year) in text:
                #     score += 2
                # if (
                #     extract_student_name(dirname).lower() in text
                #     or extract_student_name(dirname, reverse=True).lower() in text
                # ):
                #     score += 1
            assert score <= 13

            # Test of main:
            if main_:
                score += 1
                # all children should be sections
                if set(child_names(main_)) == {"section"}:
                    score += 2
                # There should be 5 h2.
                score += min(0, 5 - abs(5 - sum(1 for _ in main_.find_all("h2"))))
            assert score <= 21

            # Test for "<b>URL</b>" and "<b>URI</b>".
            uri = False
            url = False
            for b in body.find_all("b"):
                if b.text.strip() == "URI":
                    uri = True
                    if url:
                        break
                if b.text.strip() == "URL":
                    url = True
                    if uri:
                        break
            if uri:
                score += 1
            if url:
                score += 1
            assert score <= 23

            # Test of <img/>
            for img in body.find_all("img", width=500, height=400):
                score += 2
                if img.attrs.get("alt", "").strip():
                    score += 3
                src = img.attrs["src"]
                if (
                    src.startswith("../img/")
                    or src == "https://pourcelot.bitbucket.io/"
                    "img/wise-pug-thinking-about-the-world_925x.jpg"
                ):
                    score += 2
                break
            assert score <= 30

            # Test of blockquote
            score += min(2, sum(1 for _ in body.find_all("blockquote")))
            score += min(2, sum(1 for _ in body.find_all("cite")))
            assert score <= 34

            # Test of ol/li
            score += 2 * min(2, sum(1 for _ in body.find_all("ol")))
            score += 2 * min(6, sum(1 for _ in body.find_all("li")))
            assert score <= 50

            # Link to ../a_propos.html
            if body.find_all("a", href="../a_propos.html"):
                score += 4

        return score.ratio_and_log

    # ---------
    #    TD2
    # ---------

    @test(title="TD2/index.html", weight=1)
    def test_td2_index_file(self):
        score = Score(max=24)
        with CatchExceptions(score, FileNotFoundError, AttributeError):
            td1 = self.website["TD2/index.html"]
            head = td1.structure.html.head
            score += 1
            body = td1.structure.html.body
            score += 1

            link = head.find("link")
            if link is not None:
                score += 2
                if link.attrs.get("rel") == ["stylesheet"]:
                    score += 2
                if link.attrs.get("type") == "text/css":
                    score += 2
                if link.attrs.get("href") == "../css/td.css":
                    score += 1

            # BODY STRUCTURE
            headers = body.find_all("header")
            if len(headers) == 1:
                (header,) = headers
            else:
                header = None

            mains = body.find_all("main")
            if len(mains) == 1:
                (main_,) = mains
            else:
                main_ = None

            footers = body.find_all("footer")
            if len(footers) == 1:
                (footer,) = footers
            else:
                footer = None

            if child_names(body) == ["header", "main", "footer"]:
                score += 5

            # Test of header:
            if header:
                score += 1
                # contient h1 ?
                if header.find("h1"):
                    score += 2

            # Test of footer:
            if footer:
                score += 1

            # Test of main:
            if main_:
                score += 1
                # all children should be sections
                if set(child_names(main_)) == {"section"}:
                    score += 2

                # There should be one h2 title for each section.
                sections = [content for content in main_.contents if content.name == "section"]

                if len(sections) == sum(1 for _ in main_.find_all("h2")):
                    score += 3

        return score.ratio_and_log

    @test(title="index.html (2)", weight=0.5)
    def test_updated_root_index_file(self):
        score = Score(max=14)
        with CatchExceptions(score, FileNotFoundError, AttributeError):
            index = self.website["index.html"]
            score += 1
            head = index.structure.head
            body = index.structure.html.body

            link = head.find("link")
            if link is not None:
                score += 2
                if link.attrs.get("rel") == ["stylesheet"]:
                    score += 2
                if link.attrs.get("type") == "text/css":
                    score += 2
                if link.attrs.get("href") == "../css/td.css":
                    score += 1

            # Link to TD1/td1.html
            if body.find_all("a", href="TD1/index.html"):
                score += 2

            # Link to TD2/td2.html
            if body.find_all("a", href="TD2/index.html"):
                score += 2

            # Link to a_propos.html
            if body.find_all("a", href="a_propos.html"):
                score += 2
        return score.ratio_and_log

    @test(title="exo couleurs", weight=2)
    def ex_couleurs_css(self):
        score = Score(max=6)
        with CatchExceptions(score, FileNotFoundError, AttributeError):
            td2 = self.website["TD2/index.html"]
            text = td2.text.lower()
            if "#78380d" in text:
                score += 3
            if re.search(r"(?<![0-9])(14\d|150)(?![0-9])", text):
                score += 1
            if (
                re.search("(?<![0-9])(16|seize)[ ]+million(s)?", text)
                or re.search("(?<![0-9])16[ ]*777[ ]*216(?![0-9])", text)
                or re.search("256<sup>3</sup>", text)
            ):
                score += 1
            if re.search("(?<![0-9])256(?=[^0-24-9])", text):
                score += 1
        return score.ratio_and_log

    @test(title="exo DOM", weight=3)
    def ex_document_object_model(self):
        score = Score(max=26)
        with CatchExceptions(score, FileNotFoundError, AttributeError):
            td2 = self.website["TD2/index.html"]
            text = td2.text
            compact = re.sub(r"\s+", "", text)
            compact = unescape(compact).lower()  # decode HTML entities and make text lower
            answer = (
                (
                    "<body><header><h1>Les papous</h1></header>"
                    "<main><section><h2>Définition</h2>"
                    "<p>Les <em>papous</em> sont les habitants de Papouasie.</p>"
                    "</section><section><h2>Classification</h2>"
                    "<ul><li>Les papous papa.</li><li>Les papous pas papa.</li></ul>"
                    "</section></main><footer>Site réalisé par"
                    "<i>C. Levi-Strauss</i>en partenariat avec<i>A. Franquin</i>."
                    "</footer></body>"
                )
                .lower()
                .replace(" ", "")
            )
            if answer in compact:
                score += 5
            # Partial match
            start = compact.find("<body><header><h1>lespapous")
            if start != -1:
                score += 4
            end = compact.find("</body>", start)
            if end != -1:
                if remove_text_between_tags(compact[start : end + 7]) == remove_text_between_tags(
                    answer
                ):
                    score += 15
            if re.search("(?<![0-9])16n", compact):
                score += 1
            if re.search("(?<![0-9])13n", compact):
                score += 1
        return score.ratio_and_log

    @test(title="exo priorité", weight=2)
    def ex_priorite(self):
        score = Score(max=15)
        with CatchExceptions(score, FileNotFoundError, AttributeError):
            td2 = self.website["TD2/index.html"]
            text = td2.text.lower()
            compact = re.sub(r"\s+", "", text)
            if "marron" in text or "brown" in text:
                score += 1
            if "violet" in text:
                score += 1
            if "bleu" in text or "blue" in text:
                score += 1
            if score == 3:
                score += 2
            tuples_score = 0
            answered = set(re.findall(r"\(\d,\d,\d\)", compact))
            expected = {"(0,0,0)", "(0,0,1)", "(0,1,1)", "(1,0,0)"}
            for s in expected:
                if s in answered:
                    tuples_score += 2
            tuples_score -= len(answered - expected)

            if tuples_score == 8:
                tuples_score += 2
            score += max(0, tuples_score)
        return score.ratio_and_log

    @test(title="CSS TD2", weight=1)
    def test_css_td2(self):
        score = Score(max=32)
        css: CssSimpleParser
        with CatchExceptions(score, FileNotFoundError, AttributeError, CssError):
            try:
                css = self.website["css/td.css"].structure
                score += 5
            except FileNotFoundError:
                css = self.website["TD2/css/td.css"].structure

            if any(comment for comment in css.comments if not looks_like_css_code(comment)):
                score += 2
            p = css.get_properties("p")
            if "background-color" in p:
                score += 1
            if "color" in p:
                score += 1
            h1 = css.get_properties("h1")
            if "border-color" in h1:
                score += 1
            if "border-style" in h1:
                score += 1
            if "text-align" in h1:
                score += 1

            h1_over = set(css.get_properties("h1:hover"))
            if "color" in h1_over:
                score += 2
            # Test if any other color property is defined.
            other = {"border", "background-color", "border-color", "background"}
            if h1_over & other:
                score += 1
            h2_over = set(css.get_properties("h2:hover"))
            if "color" in h2_over:
                score += 2
            # Test if any other color property is defined.
            if h2_over & other:
                score += 1

            # Test for blockquote exercise
            quotes = css.get_properties("blockquote")
            quotes.update(css.get_properties("blockquote p"))
            quotes.update(css.get_properties("blockquote>p"))
            if "background-color" in quotes:
                score += 2
            before = css.get_properties("blockquote::before")
            before.update(css.get_properties("blockquote p::before"))
            before.update(css.get_properties("blockquote>p::before"))
            if "color" in before:
                score += 2
            if "content" in before:
                score += 2
                if "«" in before["content"]:
                    score += 2
            after = css.get_properties("blockquote::after")
            after.update(css.get_properties("blockquote p::after"))
            after.update(css.get_properties("blockquote>p::after"))
            if "color" in after:
                score += 2
            if "content" in after:
                score += 2
                if "»" in after["content"]:
                    score += 2

        return score.ratio_and_log

    # ---------
    #    TD3
    # ---------

    @test(title="box-sizing", weight=0.25)
    def test_star_rule(self):
        score = Score(max=5)
        css: CssSimpleParser
        with CatchExceptions(score, FileNotFoundError, AttributeError, CssError):
            try:
                css = self.website["css/td.css"].structure
                score += 1
            except FileNotFoundError:
                css = self.website["TD3/css/td.css"].structure
            star_properties = css.get_properties("*")
            if star_properties:
                score += 1
                if "box-sizing" in star_properties:
                    score += 1
                    if star_properties["box-sizing"] == "border-box":
                        score += 2
        return score.ratio_and_log

    @test(title="TD3 CSS", weight=1)
    def test_for_td3_css(self):
        score = Score(max=9)
        with CatchExceptions(score, FileNotFoundError, AttributeError, CssError):
            td3: HTMLWebFile = self.website["TD3/index.html"]
            p = td3.get_css_property("p")
            if p:
                score += 1
                if any(key.startswith("margin") for key in p):
                    score += 1
            h1 = td3.get_css_property("h1")
            if h1:
                score += 1
                if any(key.startswith("border") for key in h1):
                    score += 1
                if any(key.startswith("margin") for key in h1):
                    score += 1
                if any(key.startswith("padding") for key in h1):
                    score += 1
                if any(key.startswith("background") for key in h1):
                    score += 1
                if h1.get("max-width") == "calc(100% - 40px)":
                    score += 1
                if h1.get("width") == "80%":
                    score += 1
        return score.ratio_and_log

    @test(title="Gallerie", weight=0.5)
    def test_galerie(self):
        score = Score(max=8)
        with CatchExceptions(score, FileNotFoundError, AttributeError, CssError):
            galerie: HTMLWebFile
            try:
                galerie = self.website["TD3/galerie.html"]
            except FileNotFoundError:
                galerie = self.website[
                    "TD3/gallerie.html"
                ]  # typo in https://pourcelot.bitbucket.io/td3.html
            score += 1
            for i, path in enumerate(galerie.directly_linked_local_stylesheets):
                if i == 0 and path.name == "td.css":
                    score += 1
                if i == 1 and path.name in ("galerie.css", "gallerie.css"):
                    score += 1
            galerie_css: CSSWebFile
            try:
                galerie_css = self.website["TD3/galerie.css"]
            except FileNotFoundError:
                galerie_css = self.website[
                    "TD3/gallerie.css"
                ]  # typo in https://pourcelot.bitbucket.io/td3.html
            score += 1
            for property_name in ("box-shadow", "border-radius"):
                if any(
                    css_property.name == property_name
                    for css_property in galerie_css.structure.properties
                ):
                    score += 2
        return score.ratio_and_log

    # ---------
    #    TD4
    # ---------

    @test(title="TD4 form", weight=1)
    def test_td4_form(self):
        td4 = self.path / "TD4"
        try:
            tags = [tag for file in self.website.get_directory_html_files(td4) for tag in file.tags]
        except FileNotFoundError:
            return 0, ["Folder not found: www/R102/TD4/"]
        selectors = [
            selector
            for file in self.website.get_directory_css_files(td4)
            for selector in file.selectors
        ]
        score = 0
        if any(tag.name == "form" for tag in tags):
            score += 1
        if any(tag.name == "textarea" for tag in tags):
            score += 1
        if "label::first-letter" in selectors:
            score += 1
        inputs = [tag for tag in tags if tag.name == "input"]
        score += min(len(inputs), 4) / 2
        if sum(1 for tag in inputs if tag.attrs.get("type") == "range") == 1:
            score += 1
        if sum(1 for tag in inputs if tag.attrs.get("type") == "email") == 1:
            score += 1
        if sum(1 for tag in inputs if tag.attrs.get("type") == "url") == 1:
            score += 1
        max_score = 8
        assert score <= max_score, f"max_score should be at least {score}"
        return score / max_score, []

    @test(title="TD4 table", weight=1)
    def test_td4_table(self):
        td4 = self.path / "TD4"
        try:
            tags = [tag for file in self.website.get_directory_html_files(td4) for tag in file.tags]
        except FileNotFoundError:
            return 0, ["Folder not found: www/R102/TD4/"]
        selectors = [
            selector
            for file in self.website.get_directory_css_files(td4)
            for selector in file.selectors
        ]
        score = 0
        tables = [tag for tag in tags if tag.name == "table"]
        if tables:
            score += 1

        # Exercise about table headers, captions...
        for table in tables:
            content = str(table)
            if "téléphone" in content.lower():
                # This is the table of the first exercise (theader...)
                break
        else:
            table = None
        if table is not None:
            score += 1
            if len(table.find_all("caption")) == 1:
                score += 1
            thead_tags = [tag for tag in table.find_all("thead")]
            if len(thead_tags) == 1:
                score += 1
                thead = thead_tags[0]
                if (tr := thead.tr) is not None:
                    score += 1
                    if len(tr.find_all("th")) == 4:
                        score += 1
            tbody_tags = [tag for tag in table.find_all("tbody")]
            if len(tbody_tags) == 1:
                score += 1
                tbody = tbody_tags[0]

                if len(tbody.find_all("tr")) == 5:
                    score += 1
                if len(tbody.find_all("td")) == 20:
                    score += 1

        if "tr:nth-child(2n)" in selectors:
            score += 2
        if "tr:nth-child(2n+1)" in selectors:
            score += 2
        if "td:first-child" in selectors:
            score += 1
        if "td:first-child::first-letter" in selectors:
            score += 1
        if "th:first-child" in selectors:
            score += 1
        if "th:last-child" in selectors:
            score += 1
        before_found = False
        unicode_phone_found = False
        for file in self.website.get_directory_css_files(td4):
            for rule in file.rules:
                if rule.header.endswith(":before"):
                    for prop in rule.properties:
                        if prop.name == "content":
                            if not before_found:
                                score += 1
                                before_found = True
                            if prop.value.startswith("✆"):
                                if not unicode_phone_found:
                                    score += 1
                                    unicode_phone_found = True
                                    break
                if unicode_phone_found:
                    break
            if unicode_phone_found:
                break

        # Exercise about merged-cells
        for table in tables:
            content = str(table)
            if "300" in content and "50" in content and "200" in content and "100" in content:
                # This is the table of the merged-cells exercise.
                break
        else:
            table = None
        if table is not None:
            score += 1
            if len(table.find_all("tr")) == 5:
                score += 1
            if len(table.find_all("td")) == 8:
                score += 1
            if len(table.find_all("td", colspan=2, rowspan=2)) == 2:
                score += 1
            if len(table.find_all("td", colspan=3)) == 1:
                score += 1
            if len(table.find_all("td", rowspan=4)) == 1:
                score += 1
        max_score = 25
        assert score <= max_score, f"max_score should be at least {score}"
        return score / max_score, []

    # ---------
    #    TD5
    # ---------


def main(path=WEBSITES_PATH, output=OUTPUT_DIR, path_on_server=PATH_ON_SERVER):
    run_and_collect(R102WebsiteTest, path, output, path_on_server)


if __name__ == "__main__":
    main()
