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

import api_debug
import api_pwndoc_audit
from api_process_findings import ProcessingSettings
from tests.conftest import TMP_PATH, PWNDOC_DANGER_OVERRIDE
from tests.test_findings_parsing import test_connectivity, PwnDocCommunication
from tests.test_templating import delete_templates
from tests.helper_generate_finding_file import generate_text_for_issues
import config
from loguru import logger

# Nearly everything has a burp_ prefix, because I'm modifying an burp scan to create findings.

RESERVED_FIDS = {
    'ignored': ['burp_importer_test_ignored_fid1', 'burp_importer_test_ignored_fid2'],
    "IMPORTER_TEST_ALIAS1": ["burp_importer_test_alias_fid1", "burp_importer_test_alias_fid2"],
    "IMPORTER_TEST_ALIAS2": ["burp_importer_test_alias_fid3", "burp_importer_test_alias_fid4"],
    "IMPORTER_TEST_GROUP_1": ["IMPORTER_TEST_ALIAS2", "burp_importer_test_ignored_fid2", "burp_importer_test_fid1", "burp_importer_test_fid2"],
}


class FindingHelper:
    @staticmethod
    def partial_fids_to_full_fids(partial_fids: Set[str]) -> Set[str]:
        return set(f"burp_{partial_fid}" for partial_fid in partial_fids)

    @staticmethod
    def full_fids_to_partial_fids(full_ids: Set[str]) -> Set[str]:
        new_ids = set()
        for single_id in full_ids:
            new_ids.add(single_id.replace("burp_", "", 1))
        return new_ids

    @staticmethod
    def run_evaluation(client, test_name: str, partial_fids: Set[str], expected_fids: Set[str], profile: str = 'importer_test') -> ProcessingSettings:
        delete_templates(client)
        logger.debug(f"{test_name=} | {partial_fids=} | {expected_fids=}")

        finding_text = generate_text_for_issues(list(partial_fids))

        output_filename = f"{test_name}.json"
        folder_name = PwnDocCommunication.process_findings(
            client,
            output_name=output_filename,
            files=[(io.BytesIO(finding_text.encode()), output_filename)],
            profile=profile,
        )

        ps: ProcessingSettings = ProcessingSettings.load_from_folder_name(folder_name)
        with open(ps.get_result_path(), encoding="utf8") as f:
            data = json.load(f)

        imported_fids = set([x["fid"] for x in data])
        assert imported_fids == expected_fids
        return ps


@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 TestGroupingAndAliasing:

    @pytest.mark.parametrize("test_name, input_fids, expected_fids", [
        ('regular_import', {'burp_test1', 'burp_test2'}, {'burp_test1', 'burp_test2'}),
        ('empty_import', set(), set()),
        ('ignored_findings', set(RESERVED_FIDS['ignored']), set()),
        ('import_alias', {RESERVED_FIDS['IMPORTER_TEST_ALIAS1'][0]}, {'IMPORTER_TEST_ALIAS1'}),
        ('two_findings_into_one_alias', set(RESERVED_FIDS['IMPORTER_TEST_ALIAS2'][:]), {'IMPORTER_TEST_ALIAS2'}),  # two findings into one alias.  # todo: is this desired behavior
        ('two_findings_into_one_alias_and_then_group', {'burp_importer_test_alias_fid1', 'burp_importer_test_fid1'}, {'IMPORTER_TEST_ALIAS1', 'burp_importer_test_fid1'}),  # alias happens, but than it's not used for grouping
        ('grouping_of_findings', set(RESERVED_FIDS['IMPORTER_TEST_GROUP_1'][1:]), {'IMPORTER_TEST_GROUP_1'}),
        ('grouping_of_single_finding', {RESERVED_FIDS['IMPORTER_TEST_GROUP_1'][2]}, {RESERVED_FIDS['IMPORTER_TEST_GROUP_1'][2]}),  # when only one finding is there, it should not be grouped
    ])
    def test_all(self, client, test_name: str, input_fids: Set[str], expected_fids: Set[str]):
        input_partial_fids = FindingHelper.full_fids_to_partial_fids(input_fids)
        FindingHelper.run_evaluation(client, test_name, input_partial_fids, expected_fids)

    @classmethod
    def finding_modification_set_fids(cls, orig: dict, finding_order_id: int, fids: List[str]) -> dict:
        orig['fid'] = fids[finding_order_id]
        orig['name'] = fids[finding_order_id]
        return orig

    def test_pwndoc_grouping_two_imports(self, client):
        # This tests whether a broken grouping will trigger visual warning.  # todo: does it?
        full_fids = ["burp_importer_test_fid1", "burp_importer_test_fid2"]

        folder_name = PwnDocCommunication.setup_fake_scan2report_import(
            'tests/test_files/scan2report_results/single-finding-result',
            2,
            self.finding_modification_set_fids,
            {'fids': full_fids}
        )
        ps: ProcessingSettings = ProcessingSettings.load_from_folder_name(folder_name)
        newly_grouped_or_aliased_findings: List[Tuple[str, str, str]] = api_debug.list_newly_grouped_or_aliased_findings(ps.audit_id)

        assert len(newly_grouped_or_aliased_findings) == len(full_fids)
        assert full_fids == [x[0] for x in newly_grouped_or_aliased_findings]

    @pytest.mark.xfail(reason="After action combining is not implemented.")
    def test_after_action_combining(self, client):
        full_fids = ["burp_importer_test_fid1", "burp_importer_test_fid2"]
        folder_name = PwnDocCommunication.setup_fake_scan2report_import(
            'tests/test_files/scan2report_results/single-finding-result',
            2,
            self.finding_modification_set_fids,
            {'fids': full_fids}
        )
        ps: ProcessingSettings = ProcessingSettings.load_from_folder_name(folder_name)
        findings_scan2report_dicts: List[dict] = api_pwndoc_audit.convert_findings_to_scan2report_dicts(ps.audit_id, ps.locale)
        assert findings_scan2report_dicts[0].get("fid") == 'IMPORTER_TEST_GROUP_1'
        assert len(findings_scan2report_dicts) == 1

    def test_grouping_description(self, client):
        input_partial_fids = FindingHelper.full_fids_to_partial_fids({"burp_importer_test_fid1", "burp_importer_test_fid2"})
        ps = FindingHelper.run_evaluation(client, "test_description", input_partial_fids, {'IMPORTER_TEST_GROUP_1'})
        with open(ps.get_result_path(), encoding='utf8') as f:
            findings = json.load(f)
        assert len(findings) == 1
        description: str = findings[0]['description']

        sequence_of_items = [
            '[OG]', 'issueBackground', config.TNS_DESCRIPTION_PROOF_DELIMITER, 'issueDetail',
            '[OG]', 'issueBackground', config.TNS_DESCRIPTION_PROOF_DELIMITER, 'issueDetail',
        ]
        cur_pos = 0
        for x in sequence_of_items:
            cur_pos = description.find(x, cur_pos)
            assert cur_pos != -1, f'Description is combined in wrong order. Expected order is DESC A, PROOF A, DESC B, PROOF B. Actual description is:\n\n\n{description}'
