from typing import Dict
from unittest import mock
from unittest.mock import MagicMock, mock_open

import respx
import httpx
import pytest

from wapitiCore.wappalyzer.wappalyzer import ApplicationData, ApplicationDataException, Wappalyzer
from wapitiCore.net.response import Response


@respx.mock
@pytest.mark.asyncio
async def test_applicationdata():
    categories_file_path = "categories.txt"
    groups_file_path = "groups.txt"
    technologies_file_path = "technologies.txt"

    groups_text = """{
        "3": {
            "name": "Content"
        },
        "9": {
            "name": "Web development"
        },
        "7": {
            "name": "Servers"
        }
    }"""

    categories_text = """{
        "1": {
            "groups": [
                3
            ],
            "name": "CMS",
            "priority": 1
        },    
        "12": {
            "groups": [
                9
            ],
            "name": "JavaScript frameworks",
            "priority": 8
        },
        "27": {
            "groups": [
                9
            ],
            "name": "Programming languages",
            "priority": 5
        },
        "25": {
            "groups": [
                9
            ],
            "name": "JavaScript graphics",
            "priority": 6
        },
        "31": {
            "groups": [
            7
        ],
        "name": "CDN",
        "priority": 9
        }
    }"""

    technologies_text = """{
        "PHP": {
            "cats": [
                27
            ],
            "cookies": {
                "PHPSESSID": ""
            },
            "cpe": "cpe:/a:php:php",
            "description": "PHP is a general-purpose scripting language used for web development.",
            \"headers\": {\"Server\": \"php/?([\\\\d.]+)?\\\\;version:\\\\1\",\"X-Powered-By\": \"^php/?([\\\\d.]+)?\\\\;version:\\\\1\"},
            "icon": "PHP.svg",
            "url": \"\\\\.php(?:$|\\\\?)\",
            "website": "http://php.net"
        },
        "Akamai": {
            "cats": [
                31
            ],
            "description": "Akamai is global content delivery network (CDN) services provider for media and software delivery, and cloud security solutions.",
            "headers": {
                "X-Akamai-Transformed": "",
                "X-EdgeConnect-MidMile-RTT": "",
                "X-EdgeConnect-Origin-MEX-Latency": ""
            },
            "icon": "Akamai.svg",
            "pricing": [
                "poa"
            ],
            "saas": true,
            "website": "http://akamai.com"
        },
        "A-Frame": {
            "cats": [
                25
            ],
            "html": "<a-scene[^<>]*>",
            "icon": "A-Frame.svg",
            "implies": "three.js",
            "js": {
                \"AFRAME.version\": \"^(.+)$\\\\;version:\\\\1\"
            },
            \"scripts\": \"/?([\\\\d.]+)?/aframe(?:\\\\.min)?\\\\.js\\\\;version:\\\\1\",
            "website": "https://aframe.io"
        },
        "Angular": {
            "cats": [
                12
            ],
            "cpe": "cpe:2.3:a:angularjs:angular:*:*:*:*:*:*:*:*:*",
            "description": "Angular is a TypeScript-based open-source web application framework led by the Angular Team at Google.",
            "dom": {
                "[ng-version]": {
                    "attributes": {
                        "ng-version": "^([\\\\d\\\\.]+)\\\\;version:\\\\1"
                    }
                }
            },
            "excludes": [
                "AngularDart",
                "AngularJS"
            ],
            "icon": "Angular.svg",
            "implies": "TypeScript",
            "js": {
                "ng.coreTokens": "",
                "ng.probe": ""
            },
            "oss": true,
            "website": "https://angular.io"
        },
        "TypeScript": {
            "cats": [
                27
            ],
            "description": "TypeScript is an open-source language which builds on JavaScript by adding static type definitions.",
            "icon": "TypeScript.svg",
            "oss": true,
            "website": "https://www.typescriptlang.org"
        },
        "Umbraco": {
            "cats": [
                1
            ],
            "cookies": {
                "merchello": "\\\\;version:7"
            },
            "description": "Umbraco is an open-source Microsoft ASP.NET based content management system."
        }
    }"""

    def get_mock_open(files: Dict[str, str]):
        def open_mock(filename, *args, **kwargs):
            for expected_filename, content in files.items():
                if filename == expected_filename:
                    return mock_open(read_data=content).return_value
            raise FileNotFoundError('(mock) Unable to open {filename}')

        return MagicMock(side_effect=open_mock)

    files = {
        f'{categories_file_path}': categories_text,
        f'{groups_file_path}': groups_text,
        f'{technologies_file_path}': technologies_text,
    }

    with mock.patch("builtins.open", get_mock_open(files)):
        application_data = ApplicationData(categories_file_path, groups_file_path, technologies_file_path)

    assert application_data is not None
    assert len(application_data.get_applications()) == 6
    assert len(application_data.get_categories()) == 5
    assert len(application_data.get_groups()) == 3

    target_url = "http://perdu.com/"

    respx.get(target_url).mock(
        return_value=httpx.Response(
            200,
            text="<html><head><title>Vous Etes Perdu ?</title></head><body><adev-root ng-version=\"20.3.4\" _nghost-ng-c1178817259=\"\" ng-server-context=\"ssg\"><h1>Perdu sur l'Internet ?</h1> \
            <h2>Pas de panique, on va vous aider</h2> \
            <strong><pre>    * <----- vous &ecirc;tes ici</pre></strong></adev-root></body></html>",
            headers=[
                ('server', 'nginx/1.19.0'),
                ('content-type', 'text/html; charset=UTF-8'),
                ('x-powered-by', 'PHP/5.6.40-38+ubuntu20.04.1+deb.sury.org+1'),
                ('x-akamai-transformed', 'another text value')
            ]
        )
    )
    client = httpx.Client(cookies= {"merchello":"RANDOM"})
    resp = client.get(target_url, follow_redirects=False)
    page = Response(resp)

    wappalyzer = Wappalyzer(application_data, page, {})
    result = wappalyzer.detect()

    # Value based detection result
    assert len(result) == 5
    assert result.get("PHP") is not None
    assert len(result.get("PHP").get("categories")) == 1
    assert result.get("PHP").get("categories")[0] == "Programming languages"
    assert len(result.get("PHP").get("groups")) == 1
    assert result.get("PHP").get("groups")[0] == "Web development"
    assert result.get("PHP").get("versions")[0] == "5.6.40"

    # Key based detection result
    assert result.get("Akamai") is not None
    assert len(result.get("Akamai").get("categories")) == 1
    assert result.get("Akamai").get("categories")[0] == "CDN"
    assert len(result.get("Akamai").get("groups")) == 1
    assert result.get("Akamai").get("groups")[0] == "Servers"

    # Dom attributes detection result
    assert result.get("Angular") is not None
    assert len(result.get("Angular").get("categories")) == 1
    assert result.get("Angular").get("categories")[0] == "JavaScript frameworks"
    assert len(result.get("Angular").get("groups")) == 1
    assert result.get("Angular").get("groups")[0] == "Web development"
    assert result.get("Angular").get("versions")[0] == "20.3.4"

    # Implies detection result
    assert result.get("TypeScript") is not None
    assert len(result.get("TypeScript").get("categories")) == 1
    assert result.get("TypeScript").get("categories")[0] == "Programming languages"
    assert len(result.get("TypeScript").get("groups")) == 1
    assert result.get("TypeScript").get("groups")[0] == "Web development"

    # Client cookies detection result
    assert result.get("Umbraco") is not None
    assert len(result.get("Umbraco").get("categories")) == 1
    assert result.get("Umbraco").get("categories")[0] == "CMS"
    assert len(result.get("Umbraco").get("groups")) == 1
    assert result.get("Umbraco").get("groups")[0] == "Content"
    assert result.get("Umbraco").get("versions")[0] == "7"

    assert result.get("A-Frame") is None


def _make_mock_open(files: Dict[str, str]):
    def open_mock(filename, *args, **kwargs):
        for expected_filename, content in files.items():
            if filename == expected_filename:
                return mock_open(read_data=content).return_value
        raise FileNotFoundError(f'(mock) Unable to open {filename}')
    return MagicMock(side_effect=open_mock)


def test_applicationdata_null_technologies_raises():
    files = {
        "categories.txt": '{"1": {"groups": [3], "name": "CMS", "priority": 1}}',
        "groups.txt": '{"3": {"name": "Content"}}',
        "technologies.txt": "null",
    }
    with mock.patch("builtins.open", _make_mock_open(files)):
        with pytest.raises(ApplicationDataException):
            ApplicationData("categories.txt", "groups.txt", "technologies.txt")


def test_applicationdata_invalid_json_technologies_raises():
    files = {
        "categories.txt": '{"1": {"groups": [3], "name": "CMS", "priority": 1}}',
        "groups.txt": '{"3": {"name": "Content"}}',
        "technologies.txt": "{not valid json",
    }
    with mock.patch("builtins.open", _make_mock_open(files)):
        with pytest.raises(ApplicationDataException):
            ApplicationData("categories.txt", "groups.txt", "technologies.txt")


def test_applicationdata_null_categories_raises():
    files = {
        "categories.txt": "null",
        "groups.txt": '{"3": {"name": "Content"}}',
        "technologies.txt": '{}',
    }
    with mock.patch("builtins.open", _make_mock_open(files)):
        with pytest.raises(ApplicationDataException):
            ApplicationData("categories.txt", "groups.txt", "technologies.txt")
