import io
import json
from pathlib import Path
import os.path
from typing import List, Optional, Callable, Any, Tuple
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


# todo: check handling of images, <, /

def upload_single_template(client, template: TemplateScan2Report) -> TemplateScan2Report:
    files = [
        (io.BytesIO(template.to_json().encode()), f"{template.fid}.json")
    ]
    resp = client.post(
        '/import_automator/templates/import_scan2report_to_pwndoc',
        follow_redirects=True,
        data={
            'template_locale': template.locale,
            'file[]': files,
        },
    )
    assert resp.status_code == 200
    return template


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


@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

    def _download_from_pwndoc(self, client, fid: str, locale: str) -> TNSTemplatePwndoc:
        with client.application.app_context():
            PwndocTemplateManager.update_template_db_from_pwndoc()
            templates_and_locales = PwndocTemplateManager.get_all_templates_as_dict_of_locales()

        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 == f"{description}<p></p>"
        yield
