import glob
import html
import io
import json
from pathlib import Path
import os.path
from typing import List, Optional, Callable, Any, Tuple, Dict
import pytest
from pytest_steps import test_steps
from copy import deepcopy

from api_process_findings import ProcessingSettings
import pwndoc_api
from helpers.file_root import relative_path
from helpers.random_helpers import random_str
from template_scan2report import TemplateScan2Report
from template_manager import PwndocTemplateManager
from template_pwndoc import TNSTemplatePwndoc, HTML_ATTRIBUTES
from helpers.md_and_html_convertor import MdAndHTMLConvertorLowLevel, MdAndHTMLConvertor

from tests.conftest import TMP_PATH, PWNDOC_DANGER_OVERRIDE
from tests.test_findings_parsing import test_connectivity, PwnDocCommunication
import config
from loguru import logger


def get_scan2report_template(
        fid: Optional[str] = None,
        locale: str = "cs",
        description: Optional[str] = None
) -> TemplateScan2Report:

    if fid is None:
        fid = 'test-' + random_str(6)
    if description is None:
        description = f"Test description: {fid}\nEND"

    return TemplateScan2Report(
        fid=fid,
        locale=locale,
        severity=1,
        attack_complexicity=2,
        system_impact=3,
        name=f"Test name: {fid}",
        description=description,
        recommendations=f"Test recommendations: {fid}\nEND",
        impact=f"Test impact/observation: {fid}\nEND",
        links=[
            'http://example.com/TEST1',
            'http://example.com/TEST2',
        ],
        author="Test author",  # only in Template
        # hosts = ""  # only in Finding
        # plugin_output is only in finding and it is a dict
        ignore_pluginoutput='false'
    )


def get_pwndoc_template(
        fid: Optional[str] = None,
        locale: str = "cs",
        description: Optional[str] = None
) -> TNSTemplatePwndoc:

    if fid is None:
        fid = 'test-' + random_str(6)
    if description is None:
        description = f"Test description: {fid}\nEND"

    answer = TNSTemplatePwndoc(
        locale=locale,
        title=f"Test name: {fid}",
        description=description,
        observation=f"Test impact/observation: {fid}\nEND",
        remediation=f"Test recommendations/remediation: {fid}\nEND",
        references=[
            'http://example.com/TEST1',
            'http://example.com/TEST2',
        ],
        hosts=[
            '127.0.0.1',
            'example.com'
        ],
        poc=f"Test PoC: {fid}\nEND",
        # _is_template=True,
        fid=fid,
        severity_label="Informativní",
        attack_complexicity_label="Není k dispozici",
        system_impact_label="Není k dispozici",
        author="Test author",
        ignore_pluginoutput_checkbox=[],
    )
    answer.set_is_template(True)
    return answer


def delete_templates(client):
    client.get('/import_automator/debug/delete_vulnerability_templates')


@pytest.fixture(autouse=True)
def run_around_tests(client):
    # delete_templates(client)  # Note: Do NOT perform the delete here, otherwise EACH STEP of the test deletes the templates.
    pass
    yield
    pass  # possibly other code after test here


def upload_single_template(client, template: TemplateScan2Report) -> TemplateScan2Report:
    files = [
        (io.BytesIO(template.to_json().encode()), f"{template.fid}.json")
    ]
    upload_templates_raw(client, files, template.locale)
    return template


def upload_templates_raw(client, files: List[Tuple[io.BytesIO, str]], locale: str):
    resp = client.post(
        '/import_automator/templates/import_scan2report_to_pwndoc',
        follow_redirects=True,
        data={
            'template_locale': locale,
            'file[]': files,
        },
    )
    assert resp.status_code == 200


def upload_single_random_template_though_importer(client, description: Optional[str] = None) -> TemplateScan2Report:
    random_template = get_scan2report_template(description=description)
    uploaded_template = upload_single_template(client, random_template)
    TemplateScan2Report.delete_template_files()
    return uploaded_template


def get_template_folders() -> List[Tuple[str, str]]:
    folders_and_locales = [
        ('tests/test_files/templates/public_templates/', 'cs'),

        # Confidential templates are part of tests, but if they are not available, they will be skipped.
        ('tests/test_files/templates/confidential/cs/', 'cs'),
        ('tests/test_files/templates/confidential/en/', 'en'),
        ('tests/test_files/templates/confidential/jp/', 'en'),  # I don't have a jp locale setup
    ]
    for i in range(len(folders_and_locales)):
        if not Path(relative_path(folders_and_locales[i][0])).is_dir():
            folders_and_locales[i] = pytest.param(*folders_and_locales[i], marks=pytest.mark.skip(reason="Some input test files are not available."))

    return folders_and_locales


@pytest.mark.skipif(not PWNDOC_DANGER_OVERRIDE, reason="PWNDOC_DANGER_OVERRIDE must be allowed to enable testing of interaction with PwnDoc")
@pytest.mark.dependency(depends=["test_connectivity"])
class TestTemplateToFinding:

    def _scan2report_template_path(self, fid, locale) -> str:
        return os.path.join(
            TemplateScan2Report.native_folder_name_for_locale(locale),
            f"{fid}.json"
        )

    def _upload_template_scan2report_to_pwndoc(self, client, description: Optional[str] = None) -> Tuple[TemplateScan2Report, TNSTemplatePwndoc]:
        # Note: This tests only small part of the conversion.
        og_template: TemplateScan2Report = upload_single_random_template_though_importer(client, description=description)
        pwndoc_template = self._download_from_pwndoc(client, og_template.fid, og_template.locale)
        return og_template, pwndoc_template

    @staticmethod
    def _download_everything_from_pwndoc(client) -> Dict[str, Dict[str, TNSTemplatePwndoc]]:
        with client.application.app_context():
            PwndocTemplateManager.update_template_db_from_pwndoc()
            templates_and_locales = PwndocTemplateManager.get_all_templates_as_dict_of_locales()
        return templates_and_locales

    def _download_from_pwndoc(self, client, fid: str, locale: str) -> TNSTemplatePwndoc:
        templates_and_locales = self._download_everything_from_pwndoc(client)
        pwndoc_template: TNSTemplatePwndoc = templates_and_locales[fid][locale]
        return pwndoc_template

    def test_from_scan2report_to_pwndoc(self, client):
        delete_templates(client)
        og_template, pwndoc_template = self._upload_template_scan2report_to_pwndoc(client)

        assert pwndoc_template.fid == og_template.fid
        assert pwndoc_template.author == og_template.author

        assert pwndoc_template.description == "<p>"+og_template.description.replace("\n", "<br>")+"</p>"
        assert pwndoc_template.observation == "<p>"+og_template.impact.replace("\n", "<br>")+"</p>"
        assert pwndoc_template.remediation == "<p>"+og_template.recommendations.replace("\n", "<br>")+"</p>"

    def test_from_scan2report_to_pwndoc_more_difficult_templating(self, client):
        delete_templates(client)

        # The following description is shortened impact from a public template WEBAPP_HSTS.json.
        # It contains a lot of the potentially problematic formatting.
        description_scan2report = 'Webový […] modifikace.\n\nÚtočník […] (např. […] útočníka) je […] z HTTPS […] aplikaci.\n\n**Poznámka:** __Skutečnost, […] (přes […]) […] zabrání__.'
        description_pwndoc      = '<p>Webový […] modifikace.</p><p>Útočník […] (např. […] útočníka) je […] z HTTPS […] aplikaci.</p><p></p><p><strong>Poznámka:</strong> </p><p><em>Skutečnost, […] (přes […]) […] zabrání</em>.</p>'

        og_template, pwndoc_template = self._upload_template_scan2report_to_pwndoc(client, description=description_scan2report)

        assert pwndoc_template.description == description_pwndoc

    def _upload_scan2report_and_back(self, client, description: Optional[str] = None, md_native: bool = False) -> Tuple[TemplateScan2Report, TemplateScan2Report]:
        random_template: TemplateScan2Report = upload_single_random_template_though_importer(client, description=description)
        template_path = self._scan2report_template_path(random_template.fid, random_template.locale)

        # step: download template (in scan2report format) with fid from the pwndoc
        md_format = 'scan2report_native' if md_native else 'scan2report_with_pwndoc_formatting'
        resp = client.get(f"/import_automator/templates/download_from_pwndoc/{md_format}/{random_template.locale}")
        assert resp.status_code == 200

        with open(template_path, encoding="utf8") as f:
            recreated_data = TemplateScan2Report.from_dict(json.load(f))

        return random_template, recreated_data

    def test_from_scan2report_and_back(self, client):
        delete_templates(client)
        random_template, recreated_data = self._upload_scan2report_and_back(client, md_native=True)
        original_template_dict = random_template.to_dict()
        recreated_template_dict = recreated_data.to_dict()

        diff_keys = set()
        for og_key, og_value in original_template_dict.items():
            field_value = recreated_template_dict.get(og_key)
            if og_value != field_value:
                logger.error(f"Field '{og_key}' changed |\n og value: '{og_value}' |\n new value: '{field_value}'")
                diff_keys.add(og_key)

        assert len(diff_keys) == 0, f"At least one field changed:  See previous error msgs for more information. {diff_keys}"

    def test_from_scan2report_and_back_custom_escaped(self, client):
        delete_templates(client)
        random_template, recreated_data = self._upload_scan2report_and_back(client, md_native=False)
        assert recreated_data.description == f'pTest description: {random_template.fid}brEND/p'

    def _upload_template_pwndoc(self, client, description: Optional[str] = None, fid: Optional[str] = None) -> TNSTemplatePwndoc:
        random_template = get_pwndoc_template(description=description, fid=fid)
        with client.application.app_context():
            PwndocTemplateManager.add_single_locale_template(random_template)
        return random_template

    def _upload_template_pwndoc_and_back(self, client, description: Optional[str] = None) -> Tuple[TNSTemplatePwndoc, TNSTemplatePwndoc]:
        random_template: TNSTemplatePwndoc = self._upload_template_pwndoc(client, description=description)
        pwndoc_template = self._download_from_pwndoc(client, random_template.fid, random_template.locale)
        return random_template, pwndoc_template

    @staticmethod
    def img_description() -> str:
        return '<img src="6352e52b5137a10013dd06f2" alt="example.jpg">'  # PwnDoc requires src to be before alt

    def test_pwndoc_image(self, client):
        delete_templates(client)
        description = self.img_description()
        # Uploading escaped description is bit of a hack, but it would simulate situation when the html (here image tag) would be set from PwnDoc UI.
        escaped_description = MdAndHTMLConvertorLowLevel.custom_escape_html(description)

        og_template, pwndoc_template = self._upload_template_pwndoc_and_back(client, description=escaped_description)
        assert pwndoc_template.description == description

    def test_pwndoc_image_from_incorrect(self, client):
        delete_templates(client)
        description = f"BEFORE{self.img_description()}<p>AFTER</p>"
        expected_output = f"<p>BEFORE</p>{self.img_description()}<p>AFTER</p>"

        # Uploading escaped description is bit of a hack, but it would simulate situation when the html (here image tag) would be set from PwnDoc UI.
        escaped_description = MdAndHTMLConvertorLowLevel.custom_escape_html(description)

        og_template, pwndoc_template = self._upload_template_pwndoc_and_back(client, description=escaped_description)
        assert pwndoc_template.description == expected_output

    def _prepare_burp_finding(self, partial_burp_fid: str) -> Tuple[str, str]:
        raw_finding = relative_path("tests/test_files/scanner_results/DefectDojo/burp/one_finding_with_blank_response.xml")
        og_burp_id = "1049088"

        with open(raw_finding, encoding="utf8") as f:
            finding_text = f.read()
        finding_text = finding_text.replace(og_burp_id, partial_burp_fid)
        return finding_text, "burp_"+partial_burp_fid

    @test_steps('Delete existing templates', 'Upload Template with HTML', 'Process finding to JSON', 'Upload Finding to PwnDoc Audit', 'Download PwnDoc audit report', 'Compare original description with the new one')
    def test_template_to_finding_consistency(self, client):
        delete_templates(client)
        yield

        finding_text, full_fid = self._prepare_burp_finding("test_template_to_finding_consistency")

        # step: add HTML template
        description = self.img_description()
        escaped_description = MdAndHTMLConvertorLowLevel.custom_escape_html(description)
        og_template: TNSTemplatePwndoc = self._upload_template_pwndoc(client, description=escaped_description, fid=full_fid)
        yield

        output_filename = f"{full_fid}.json"

        # step: import finding
        folder_name = PwnDocCommunication.process_findings(
            client,
            output_name=output_filename,
            files=[(io.BytesIO(finding_text.encode()), output_filename)]
        )
        yield

        # step: upload processed finding to PwnDoc
        PwnDocCommunication.upload_proccessed_findings_to_pwndoc(folder_name, output_filename)
        yield

        # step: download audit and try to convert it to scan2report template
        ps: ProcessingSettings = ProcessingSettings.load_from_folder_name(folder_name)
        audit_data = pwndoc_api.download_and_return_audit(ps.audit_id)
        new_description = audit_data['findings'][0]['description']
        yield

        # step: verify the consistency
        assert new_description.startswith(f"{description}<p>===")  # The proof is quite long
        yield

    @pytest.mark.parametrize("folder_path, locale", get_template_folders())
    def test_import_multiple_templates_check_no_html(self, client, folder_path: str, locale: str):
        delete_templates(client)

        wildcard_filepath = relative_path(f"{folder_path}/*.json")
        template_filepaths = glob.glob(wildcard_filepath)

        files = []
        for template_filepath in template_filepaths:
            with open(template_filepath, 'r', encoding="utf8") as f:
                data = json.load(f)

            base_filename = str(os.path.basename(template_filepath))
            fid = base_filename.rsplit(".", 1)[0]
            locale = locale

            template_scan2report = TemplateScan2Report.parse_dict_custom(fid, locale, data)
            files.append(
                (io.BytesIO(template_scan2report.to_json().encode()), f"{template_scan2report.fid}.json")
            )

        upload_templates_raw(client, files, locale)

        self.validate_no_escaped_html_in_templates(client)

    def validate_no_escaped_html_in_templates(self, client):
        forbidden_tags = [html.escape(x) for x in ["<br>", "<em>", "<p>"]]

        templates_and_locales = self._download_everything_from_pwndoc(client)
        assert len(templates_and_locales) >= 1

        for fid in templates_and_locales:
            for locale in templates_and_locales[fid]:
                for field_name in HTML_ATTRIBUTES:
                    val = getattr(templates_and_locales[fid][locale], field_name)
                    for single_escaped_tag in forbidden_tags:
                        assert single_escaped_tag not in val, f"Escaped HTML tag found in template | FID: {fid} | locale: {locale} | field name: {field_name} | escaped tag: '{single_escaped_tag}'"

    def test_multilang_import(self, client):
        delete_templates(client)
        base_path = 'tests/test_files/templates/multilang/'
        locales = ['cs', 'en']
        fid = 'WEBAPP_HSTS'
        filename = f'{fid}.json'

        for locale in locales:
            template_filepath = os.path.join(base_path, locale, filename)
            with open(template_filepath, 'r', encoding="utf8") as f:
                data = json.load(f)

            template_scan2report = TemplateScan2Report.parse_dict_custom(fid, locale, data)
            upload_templates_raw(client, [(io.BytesIO(template_scan2report.to_json().encode()), f"{template_scan2report.fid}.json")], template_scan2report.locale)

            self.validate_no_escaped_html_in_templates(client)

# todo: test SSH_insecure_configuration - look for newline before "Cipher Block Chaining, CBC"
