# Copyright (C) 2015-2022  The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information

import textwrap

import pytest

from rest_framework.response import Response

from swh.storage.exc import StorageAPIError, StorageDBError
from swh.web.api.apidoc import _parse_httpdomain_doc, api_doc
from swh.web.api.apiurls import api_route
from swh.web.tests.django_asserts import assert_contains, assert_not_contains
from swh.web.tests.helpers import check_api_get_responses, check_html_get_response
from swh.web.utils import prettify_html, reverse
from swh.web.utils.exc import BadInputExc, ForbiddenExc, NotFoundExc

_httpdomain_doc = """
.. http:get:: /api/1/revision/(sha1_git)/

    Get information about a revision in the archive.
    Revisions are identified by **sha1** checksums, compatible with Git commit
    identifiers.
    See :func:`swh.model.git_objects.revision_git_object` in our data model
    module for details about how they are computed.

    :param string sha1_git: hexadecimal representation of the revision
        **sha1_git** identifier

    :reqheader Accept: the requested response content type,
        either ``application/json`` (default) or ``application/yaml``
    :resheader Content-Type: this depends on :http:header:`Accept` header
        of request

    :<json int n: sample input integer
    :<json string s: sample input string
    :<json array a: sample input array

    :>json object author: information about the author of the revision
    :>json object committer: information about the committer of the revision
    :>json string committer_date: RFC3339 representation of the commit date
    :>json string date: RFC3339 representation of the revision date
    :>json string directory: the unique identifier that revision points to
    :>json string directory_url: link to
        :http:get:`/api/1/directory/(sha1_git)/[(path)/]` to get information
        about the directory associated to the revision
    :>json string id: the revision unique identifier
    :>json boolean merge: whether or not the revision corresponds to a merge
        commit
    :>json string message: the message associated to the revision
    :>json array parents: the parents of the revision, i.e. the previous
        revisions that head directly to it, each entry of that array contains
        an unique parent revision identifier but also a link to
        :http:get:`/api/1/revision/(sha1_git)/` to get more information
        about it
    :>json string type: the type of the revision

    :statuscode 200: no error
    :statuscode 400: an invalid **sha1_git** value has been provided
    :statuscode 404: requested revision can not be found in the archive

    **Example:**

    .. parsed-literal::

        :swh_web_api:`revision/aafb16d69fd30ff58afdd69036a26047f3aebdc6/`
"""


_exception_http_code = {
    BadInputExc: 400,
    ForbiddenExc: 403,
    NotFoundExc: 404,
    Exception: 500,
    StorageAPIError: 503,
    StorageDBError: 503,
}


def test_apidoc_nodoc_failure():
    with pytest.raises(Exception):

        @api_doc("/my/nodoc/url/", "test")
        def apidoc_nodoc_tester(request, arga=0, argb=0):
            return Response(arga + argb)


@api_route(r"/some/(?P<myarg>[0-9]+)/(?P<myotherarg>[0-9]+)/", "api-1-some-doc-route")
@api_doc("/some/doc/route/", category="test")
def apidoc_route(request, myarg, myotherarg, akw=0):
    """
    Sample doc
    """
    return {"result": int(myarg) + int(myotherarg) + akw}


def test_apidoc_route_doc(client):
    api_view_name = "api-1-some-doc-route-doc"
    doc_url = reverse(api_view_name)
    assert doc_url == "/api/1/some/doc/route/doc/"
    resp = check_html_get_response(
        client, doc_url, status_code=200, template_used="apidoc.html"
    )

    # check apidoc breadcrumbs links
    api_view_name_split = api_view_name.split("-")
    for i in range(2, len(api_view_name_split) - 1):
        sub_doc_url = "/" + ("/".join(api_view_name_split[:i])) + "/doc/"
        assert_not_contains(resp, f'<a href="{sub_doc_url}">')
    assert_contains(resp, f'<a href="{doc_url}">')

    # check previous erroneous URL now redirects to the fixed one
    url = reverse("1-some-doc-route-doc")
    resp = check_html_get_response(client, url, status_code=302)
    assert resp["location"] == doc_url


def test_apidoc_route_fn(api_client):
    url = reverse("api-1-some-doc-route", url_args={"myarg": 1, "myotherarg": 1})
    check_api_get_responses(api_client, url, status_code=200)


@api_route(r"/test/error/(?P<exc_name>.+)/", "api-1-test-error")
@api_doc("/test/error/", category="test")
def apidoc_test_error_route(request, exc_name):
    """
    Sample doc
    """
    for e in _exception_http_code.keys():
        if e.__name__ == exc_name:
            raise e("Error")


def test_apidoc_error(api_client):
    for exc, code in _exception_http_code.items():
        url = reverse("api-1-test-error", url_args={"exc_name": exc.__name__})
        check_api_get_responses(api_client, url, status_code=code)


@api_route(
    r"/some/full/(?P<myarg>[0-9]+)/(?P<myotherarg>[0-9]+)/",
    "api-1-some-complete-doc-route",
)
@api_doc("/some/complete/doc/route/", category="test")
def apidoc_full_stack(request, myarg, myotherarg, akw=0):
    """
    Sample doc
    """
    return {"result": int(myarg) + int(myotherarg) + akw}


def test_apidoc_full_stack_doc(client):
    url = reverse("api-1-some-complete-doc-route-doc")
    check_html_get_response(client, url, status_code=200, template_used="apidoc.html")


def test_apidoc_full_stack_fn(api_client):
    url = reverse(
        "api-1-some-complete-doc-route", url_args={"myarg": 1, "myotherarg": 1}
    )
    check_api_get_responses(api_client, url, status_code=200)


@api_route(r"/test/post/only/", "api-1-test-post-only", methods=["POST"])
@api_doc("/test/post/only/", category="test")
def apidoc_test_post_only(request, exc_name):
    """
    Sample doc
    """
    return {"result": "some data"}


def test_apidoc_post_only(client):
    # a dedicated view accepting GET requests should have
    # been created to display the HTML documentation
    url = reverse("api-1-test-post-only-doc")
    check_html_get_response(client, url, status_code=200, template_used="apidoc.html")


def test_api_doc_parse_httpdomain():
    doc_data = {
        "description": "",
        "urls": [],
        "args": [],
        "params": [],
        "resheaders": [],
        "reqheaders": [],
        "input_type": "",
        "inputs": [],
        "return_type": "",
        "returns": [],
        "status_codes": [],
        "examples": [],
    }

    _parse_httpdomain_doc(_httpdomain_doc, doc_data)

    expected_urls = [
        {
            "rule": "/api/1/revision/ **\\(sha1_git\\)** /",
            "methods": ["GET", "HEAD", "OPTIONS"],
        }
    ]

    assert "urls" in doc_data
    assert doc_data["urls"] == expected_urls

    expected_description = (
        "Get information about a revision in the archive. "
        "Revisions are identified by **sha1** checksums, "
        "compatible with Git commit identifiers. See "
        "**swh.model.git_objects.revision_git_object** in "
        "our data model module for details about how they "
        "are computed."
    )

    assert "description" in doc_data
    assert doc_data["description"] == expected_description

    expected_args = [
        {
            "name": "sha1_git",
            "type": "string",
            "doc": (
                "hexadecimal representation of the revision " "**sha1_git** identifier"
            ),
        }
    ]

    assert "args" in doc_data
    assert doc_data["args"] == expected_args

    expected_params = []
    assert "params" in doc_data
    assert doc_data["params"] == expected_params

    expected_reqheaders = [
        {
            "doc": (
                "the requested response content type, either "
                "``application/json`` (default) or ``application/yaml``"
            ),
            "name": "Accept",
        }
    ]

    assert "reqheaders" in doc_data
    assert doc_data["reqheaders"] == expected_reqheaders

    expected_resheaders = [
        {"doc": "this depends on **Accept** header of request", "name": "Content-Type"}
    ]

    assert "resheaders" in doc_data
    assert doc_data["resheaders"] == expected_resheaders

    expected_statuscodes = [
        {"code": "200", "doc": "no error"},
        {"code": "400", "doc": "an invalid **sha1_git** value has been provided"},
        {"code": "404", "doc": "requested revision can not be found in the archive"},
    ]

    assert "status_codes" in doc_data
    assert doc_data["status_codes"] == expected_statuscodes

    expected_input_type = "object"

    assert "input_type" in doc_data
    assert doc_data["input_type"] == expected_input_type

    expected_inputs = [
        {"name": "n", "type": "int", "doc": "sample input integer"},
        {"name": "s", "type": "string", "doc": "sample input string"},
        {"name": "a", "type": "array", "doc": "sample input array"},
    ]

    assert "inputs" in doc_data
    assert doc_data["inputs"] == expected_inputs

    expected_return_type = "object"

    assert "return_type" in doc_data
    assert doc_data["return_type"] == expected_return_type

    expected_returns = [
        {
            "name": "author",
            "type": "object",
            "doc": "information about the author of the revision",
        },
        {
            "name": "committer",
            "type": "object",
            "doc": "information about the committer of the revision",
        },
        {
            "name": "committer_date",
            "type": "string",
            "doc": "RFC3339 representation of the commit date",
        },
        {
            "name": "date",
            "type": "string",
            "doc": "RFC3339 representation of the revision date",
        },
        {
            "name": "directory",
            "type": "string",
            "doc": "the unique identifier that revision points to",
        },
        {
            "name": "directory_url",
            "type": "string",
            "doc": (
                "link to `/api/1/directory/ </api/1/directory/doc/>`_ "
                "to get information about the directory associated to "
                "the revision"
            ),
        },
        {"name": "id", "type": "string", "doc": "the revision unique identifier"},
        {
            "name": "merge",
            "type": "boolean",
            "doc": "whether or not the revision corresponds to a merge commit",
        },
        {
            "name": "message",
            "type": "string",
            "doc": "the message associated to the revision",
        },
        {
            "name": "parents",
            "type": "array",
            "doc": (
                "the parents of the revision, i.e. the previous revisions "
                "that head directly to it, each entry of that array "
                "contains an unique parent revision identifier but also a "
                "link to `/api/1/revision/ </api/1/revision/doc/>`_ "
                "to get more information about it"
            ),
        },
        {"name": "type", "type": "string", "doc": "the type of the revision"},
    ]

    assert "returns" in doc_data
    assert doc_data["returns"] == expected_returns

    expected_examples = ["/api/1/revision/aafb16d69fd30ff58afdd69036a26047f3aebdc6/"]

    assert "examples" in doc_data
    assert doc_data["examples"] == expected_examples


@api_route(r"/post/endpoint/", "api-1-post-endpoint", methods=["POST"])
@api_doc("/post/endpoint/", category="test")
def apidoc_test_post_endpoint(request):
    """
    .. http:post:: /api/1/post/endpoint/

        Endpoint documentation

        :<jsonarr string -: Input array of SWHIDs

        :>json object <swhid>: an object whose keys are input SWHIDs
            and values objects with the following keys:

                * **known (bool)**: whether the object was found

    """
    pass


def test_apidoc_input_output_doc(client):
    url = reverse("api-1-post-endpoint-doc")
    rv = check_html_get_response(
        client, url, status_code=200, template_used="apidoc.html"
    )

    input_html_doc = textwrap.indent(
        (
            '<dl class="row">\n'
            ' <dt class="col col-md-2 text-right">\n'
            "  array\n"
            " </dt>\n"
            ' <dd class="col col-md-9">\n'
            "  <p>\n"
            "   Input array of SWHIDs\n"
            "  </p>\n"
            " </dd>\n"
            "</dl>\n"
        ),
        " " * 7,
    )

    output_html_doc = textwrap.indent(
        (
            '<dl class="row">\n'
            ' <dt class="col col-md-2 text-right">\n'
            "  object\n"
            " </dt>\n"
            ' <dd class="col col-md-9">\n'
            "  <p>\n"
            "   an object containing the following keys:\n"
            "  </p>\n"
            '  <div class="swh-rst">\n'
            "   <blockquote>\n"
            "    <ul>\n"
            "     <li>\n"
            "      <p>\n"
            "       <strong>\n"
            "        &lt;swhid&gt; (object)\n"
            "       </strong>\n"
            "       :           an object whose keys are input SWHIDs"
            " and values objects with the following keys:\n"
            "      </p>\n"
            "      <blockquote>\n"
            '       <ul class="simple">\n'
            "        <li>\n"
            "         <p>\n"
            "          <strong>\n"
            "           known (bool)\n"
            "          </strong>\n"
            "          : whether the object was found\n"
            "         </p>\n"
            "        </li>\n"
            "       </ul>\n"
            "      </blockquote>\n"
            "     </li>\n"
            "    </ul>\n"
            "   </blockquote>\n"
            "  </div>\n"
            " </dd>\n"
            "</dl>\n"
        ),
        " " * 7,
    )

    html = prettify_html(rv.content)

    assert input_html_doc in html
    assert output_html_doc in html


@api_route(r"/endpoint/links/in/doc/", "api-1-endpoint-links-in-doc")
@api_doc("/endpoint/links/in/doc/", category="test")
def apidoc_test_endpoint_with_links_in_doc(request):
    """
    .. http:get:: /api/1/post/endpoint/

        Endpoint documentation with links to
        :http:get:`/api/1/content/[(hash_type):](hash)/`,
        :http:get:`/api/1/directory/(sha1_git)/[(path)/]`
        and `archive <https://archive.softwareheritage.org>`_.
    """
    pass


def test_apidoc_with_links(client):
    url = reverse("api-1-endpoint-links-in-doc")
    rv = check_html_get_response(
        client, url, status_code=200, template_used="apidoc.html"
    )

    html = prettify_html(rv.content)

    first_link = textwrap.indent(
        (
            '<a class="reference external" href="/api/1/content/doc/">\n'
            " /api/1/content/\n"
            "</a>"
        ),
        " " * 9,
    )

    second_link = textwrap.indent(
        (
            '<a class="reference external" href="/api/1/directory/doc/">\n'
            " /api/1/directory/\n"
            "</a>"
        ),
        " " * 9,
    )

    third_link = textwrap.indent(
        (
            '<a class="reference external" '
            'href="https://archive.softwareheritage.org">\n'
            " archive\n"
            "</a>"
        ),
        " " * 9,
    )

    assert first_link in html
    assert second_link in html
    assert third_link in html
