#!/usr/bin/env python3
"""
Tests and helper script for NotaBLE API.

From the admin development environment:

The API spec file can be generated with:
    python -m admin.views.test.test_ApiViews --generate-spec


A simple testing ground that serves the Swagger UI can also be started with:
    python -m admin.views.test.test_ApiViews


NotaBLE és la col·laboració entre Gwido i el Workspace educatiu DD.

És un projecte de Xnet, IsardVDI, Gwido i Taller de Músics,
guanyador de la Ciutat Proactiva 2021, suport a la innovació urbana de
la Fundació BitHabitat.
"""

import copy
import os
from tempfile import TemporaryDirectory
from typing import TYPE_CHECKING, Any, Dict, List, Optional

import flask_unittest
from flask.testing import FlaskClient
from jose import jws

if TYPE_CHECKING:
    from werkzeug.test import TestResponse

from admin.flaskapp import AdminFlaskApp
from admin.lib.admin import MANAGER, STUDENT, TEACHER
from admin.views.ApiViews import group_parser, role_parser, user_parser
from admin.views.test.mocks import MockAdmin, MockKeycloak


def _testApp() -> AdminFlaskApp:
    """
    Helper function to generate a testableFlask App
    """
    app = AdminFlaskApp(
        "TestApp", root_path="src/admin", template_folder="static/templates"
    )
    # Patch the admin
    app.admin = MockAdmin(app)
    # Patch keycloak client
    app.admin.keycloak = MockKeycloak(app)
    # Add socketio
    from flask_socketio import SocketIO

    app.socketio = SocketIO(app)
    return app


class ApiViewsTests(flask_unittest.ClientTestCase):
    #
    # Instance fields
    #
    _app: Optional[AdminFlaskApp] = None
    secret: str = "TestSecret"
    tmpdir: TemporaryDirectory

    #
    # Instance properties
    #
    @property
    def app(self) -> AdminFlaskApp:
        if not self._app:
            os.environ["DOMAIN"] = "dd-test.example"
            os.environ["API_SECRET"] = self.secret
            self._app = _testApp()
        return self._app

    @property
    def auth_header(self) -> Dict[str, str]:
        token = jws.sign({}, self.secret, algorithm="HS256")
        return {"Authorization": f"bearer {token}"}

    #
    # Test setup
    #
    def setUp(self, client: FlaskClient) -> None:
        self.tmpdir = TemporaryDirectory()
        os.environ["NC_MAIL_QUEUE_FOLDER"] = os.path.join(
            str(self.tmpdir.name), "nc_queue"
        )

    def tearDown(self, client: FlaskClient) -> None:
        self.tmpdir.cleanup()
        self._app = None

    #
    # Helper functions
    #
    def _ur(
        self,
        client: FlaskClient,
        route: str,
        method: str = "GET",
        response_code: int = 401,
    ) -> "TestResponse":
        """
        Helper function to ensure endpoints without authenticating
        """
        rv = client.open(route, method=method)
        self.assertStatus(rv, response_code)
        return rv

    def _r(
        self,
        client: FlaskClient,
        route: str,
        method: str = "GET",
        response_code: int = 200,
        *args: Any,
        **kwargs: Any,
    ) -> "TestResponse":
        """
        Helper function to test endpoint functionality with authentication
        """
        rv = client.open(
            route, method=method, headers=self.auth_header, *args, **kwargs
        )
        self.assertStatus(rv, response_code)
        return rv

    _bad_json: List[Any] = [None, [], "test", 1, 0, True, False, {}]

    _initialUsers = [
        {
            "username": "u1",
            "id": "u1",
            "keycloak": True,
            "keycloak_id": "u1",
            "enabled": True,
            "email": "u1@u.com",
            "first": "u",
            "firstname": "u",
            "last": "1",
            "lastname": "1",
            "roles": [MANAGER],
            "keycloak_groups": ["group1"],
            "groups": ["group1", MANAGER],
            "moodle": True,
            "moodle_id": "u1",
            "moodle_groups": [],
            "nextcloud": True,
            "nextcloud_id": "u1",
            "nextcloud_groups": [],
            "quota": False,
            "quota_used_bytes": False,
        },
    ]

    _initialGroups = [
        {"keycloak": True, "id": r, "name": r, "path": r}
        for r in ["group1", MANAGER, STUDENT, TEACHER]
    ]
    _initialRoles = [
        {"id": r, "name": f"NAME {r}", "description": f"DESC {r}"}
        for r in [MANAGER, STUDENT, TEACHER]
    ]

    def _feedInitialData(self) -> None:
        self.app.admin.internal["users"] = copy.deepcopy(self._initialUsers)
        self.app.admin.internal["groups"] = copy.deepcopy(self._initialGroups)
        self.app.admin.internal["roles"] = copy.deepcopy(self._initialRoles)

    def _canonicUserData(self, users: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        return [user_parser(u) for u in users]

    def _canonicGroupData(self, groups: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        return [group_parser(g) for g in groups]

    def _canonicRoleData(self, roles: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        return [role_parser(r) for r in roles]

    #
    # Actual tests
    #

    # /ddapi/users
    def test_users_401(self, client: FlaskClient) -> None:
        for m in ["GET"]:
            self._ur(client, "/ddapi/users", method=m)

    def test_users_405(self, client: FlaskClient) -> None:
        for m in ["PUT", "DELETE", "POST"]:
            self._ur(client, "/ddapi/users", method=m, response_code=405)
            self._r(client, "/ddapi/users", method=m, response_code=405)

    def test_users_GET(self, client: FlaskClient) -> None:
        """Ensure GETting users makes sense"""
        rv = self._r(client, "/ddapi/users")
        # When empty, it should return an empty list
        self.assertEqual([], rv.json, msg="Expected empty list")

        # When populated, it should return the test data
        self._feedInitialData()
        rv = self._r(client, "/ddapi/users")
        self.assertEqual(
            self._canonicUserData(self._initialUsers),
            rv.json,
            msg="Unexpected data received",
        )

    # /ddapi/users/filter
    # Searches in username, first, last, email
    def test_users_filter_401(self, client: FlaskClient) -> None:
        for m in ["POST"]:
            self._ur(client, "/ddapi/users/filter", method=m)

    def test_users_filter_405(self, client: FlaskClient) -> None:
        for m in ["GET", "DELETE", "PUT"]:
            self._ur(client, "/ddapi/users/filter", method=m, response_code=405)
            self._r(client, "/ddapi/users/filter", method=m, response_code=405)

    def test_users_filter_POST(self, client: FlaskClient) -> None:
        rv = self._r(client, "/ddapi/users/filter", method="POST", response_code=400)

        for bad_j in self._bad_json:
            rv = self._r(
                client,
                "/ddapi/users/filter",
                method="POST",
                response_code=400,
                json=bad_j,
            )

        rv = self._r(client, "/ddapi/users/filter", method="POST", json={"text": "u1"})
        self.assertEqual([], rv.json, "Unexpected data received")
        self._feedInitialData()
        rv = self._r(client, "/ddapi/users/filter", method="POST", json={"text": "u1"})
        self.assertEqual(
            self._canonicUserData(self._initialUsers),
            rv.json,
            "Unexpected data received",
        )
        rv = self._r(
            client,
            "/ddapi/users/filter",
            method="POST",
            json={"text": "SEARCHING_FOR_NO_MATCH"},
        )
        self.assertEqual([], rv.json, "Unexpected data received")

    # /ddapi/groups
    def test_groups_401(self, client: FlaskClient) -> None:
        for m in ["GET"]:
            self._ur(client, "/ddapi/groups", method=m)

    def test_groups_405(self, client: FlaskClient) -> None:
        for m in ["POST", "PUT", "DELETE"]:
            self._ur(client, "/ddapi/groups", method=m, response_code=405)
            self._r(client, "/ddapi/groups", method=m, response_code=405)

    def test_groups_GET(self, client: FlaskClient) -> None:
        rv = self._r(client, "/ddapi/groups")
        self.assertEqual([], rv.json, "Unexpected data received")
        self._feedInitialData()
        rv = self._r(client, "/ddapi/groups")
        self.assertEqual(
            self._canonicGroupData(self._initialGroups),
            rv.json,
            "Unexpected data received",
        )

    # /ddapi/roles
    def test_roles_401(self, client: FlaskClient) -> None:
        for m in ["GET"]:
            self._ur(client, "/ddapi/roles", method=m)

    def test_roles_405(self, client: FlaskClient) -> None:
        for m in ["POST", "DELETE", "PUT"]:
            self._ur(client, "/ddapi/roles", method=m, response_code=405)
            self._r(client, "/ddapi/roles", method=m, response_code=405)

    def test_roles_GET(self, client: FlaskClient) -> None:
        rv = self._r(client, "/ddapi/roles")
        self.assertEqual([], rv.json, "Unexpected data received")
        self._feedInitialData()
        rv = self._r(client, "/ddapi/roles")
        self.assertEqual(
            self._canonicRoleData(self._initialRoles),
            rv.json,
            "Unexpected data received",
        )

    # /ddapi/role/users
    # - id|name|keycloak_id <-- used in this order, equivalent
    def test_role_users_405(self, client: FlaskClient) -> None:
        for m in ["GET", "DELETE", "PUT"]:
            self._ur(client, "/ddapi/role/users", method=m, response_code=405)
            self._r(client, "/ddapi/role/users", method=m, response_code=405)

    def test_role_users_401(self, client: FlaskClient) -> None:
        for m in ["POST"]:
            self._ur(client, "/ddapi/role/users", method=m)

    def test_role_users_POST(self, client: FlaskClient) -> None:
        rv = self._r(client, "/ddapi/role/users", method="POST", response_code=400)
        for bad_j in self._bad_json:
            rv = self._r(
                client,
                "/ddapi/role/users",
                method="POST",
                response_code=400,
                json=bad_j,
            )
        rv = self._r(client, "/ddapi/role/users", method="POST", json={"id": MANAGER})
        self.assertEqual([], rv.json, "Unexpected data received")
        self._feedInitialData()
        rv = self._r(client, "/ddapi/role/users", method="POST", json={"id": MANAGER})
        self.assertEqual(
            self._canonicUserData(self._initialUsers),
            rv.json,
            "Unexpected data received",
        )
        rv = self._r(client, "/ddapi/role/users", method="POST", json={"id": TEACHER})
        self.assertEqual([], rv.json, "Unexpected data received")

    # /ddapi/user[/<user_ddid>]
    def test_user_401(self, client: FlaskClient) -> None:
        for m in ["POST"]:
            self._ur(client, "/ddapi/user", method=m)

    def test_user_405(self, client: FlaskClient) -> None:
        for m in ["GET", "DELETE", "PUT"]:
            self._ur(client, "/ddapi/user", method=m, response_code=405)
            self._r(client, "/ddapi/user", method=m, response_code=405)

    def test_user_POST(self, client: FlaskClient) -> None:
        self._r(client, "/ddapi/user", method="POST", response_code=400)
        for bad_j in self._bad_json:
            self._r(client, "/ddapi/user", method="POST", response_code=400, json=bad_j)
        new_user = {
            "email": "a",
            "enabled": True,
            "password": "PASS",
            "quota": "",
            "username": "cu1",
            "first": "FIRSTNAME",
            "last": "LASTNAME",
            "role": MANAGER,
            "groups": ["group2"],
        }
        self._r(client, "/ddapi/user", method="POST", response_code=400, json=new_user)
        self.assertNotIn(
            "cu1", [u["id"] for u in self.app.admin.internal["users"]], "Created user"
        )
        new_user["email"] = "a@a.example"
        self._r(client, "/ddapi/user", method="POST", response_code=412, json=new_user)
        self.assertNotIn(
            "cu1", [u["id"] for u in self.app.admin.internal["users"]], "Created user"
        )
        self._feedInitialData()
        self._r(client, "/ddapi/user", method="POST", response_code=412, json=new_user)
        self.assertNotIn(
            "cu1", [u["id"] for u in self.app.admin.internal["users"]], "Created user"
        )
        new_user["groups"] = ["group1"]
        self._r(client, "/ddapi/user", method="POST", response_code=200, json=new_user)
        self.assertIn(
            "cu1",
            [u["id"] for u in self.app.admin.internal["users"]],
            "Did not create user",
        )

    def test_user_ddid_GET_401(self, client: FlaskClient) -> None:
        for m in ["GET", "DELETE", "PUT"]:
            self._ur(client, "/ddapi/user/u1", method=m)

    def test_user_ddid_GET_405(self, client: FlaskClient) -> None:
        for m in ["POST"]:
            self._ur(client, "/ddapi/user/u1", method=m, response_code=405)
            self._r(client, "/ddapi/user/u1", method=m, response_code=405)

    def test_user_ddid_GET(self, client: FlaskClient) -> None:
        self._r(client, "/ddapi/user/", response_code=405)
        self._r(client, "/ddapi/user/u1", response_code=404)
        self._feedInitialData()
        rv = self._r(client, "/ddapi/user/u1", response_code=200)
        self.assertEqual("u1", rv.json["username"])

    def test_user_ddid_DELETE(self, client: FlaskClient) -> None:
        self._r(client, "/ddapi/user/u1", method="DELETE", response_code=404)
        self._feedInitialData()
        rv = self._r(client, "/ddapi/users")
        self.assertNotEqual(rv.json, [])
        self._r(client, "/ddapi/user/u1", method="DELETE")
        rv = self._r(client, "/ddapi/users")
        self.assertEqual(rv.json, [])
        rv = self._r(client, "/ddapi/user/u1", response_code=404)

    def test_user_ddid_PUT(self, client: FlaskClient) -> None:
        self._r(client, "/ddapi/user/u1", method="PUT", response_code=404)
        self._feedInitialData()
        rv = self._r(client, "/ddapi/user/u1")
        prev_u = rv.json
        self.assertEqual(prev_u["first"], "u")
        self._r(client, "/ddapi/user/u1", method="PUT", response_code=400)
        for bad_j in self._bad_json:
            self._r(
                client, "/ddapi/user/u1", method="PUT", response_code=400, json=bad_j
            )
        rv = self._r(client, "/ddapi/user/u1")
        self.assertEqual(prev_u, rv.json)
        self._r(
            client,
            "/ddapi/user/u1",
            method="PUT",
            response_code=200,
            json={"first": "U"},
        )
        rv = self._r(client, "/ddapi/user/u1")
        new_u = rv.json
        self.assertNotEqual(prev_u, rv.json)
        self.assertEqual(rv.json["first"], "U")
        prev_u = new_u
        self._r(
            client,
            "/ddapi/user/u1",
            method="PUT",
            response_code=200,
            json={"first": "U", "password": "TEST2"},
        )

    # /ddapi/username/OLD/NEW
    def test_username_401(self, client: FlaskClient) -> None:
        for m in ["PUT"]:
            self._ur(client, "/ddapi/username/a/b", method=m)

    def test_username_405(self, client: FlaskClient) -> None:
        for m in ["GET", "DELETE", "POST"]:
            self._ur(client, "/ddapi/username/a/b", method=m, response_code=405)
            self._r(client, "/ddapi/username/a/b", method=m, response_code=405)

    def test_username_PUT(self, client: FlaskClient) -> None:
        rv = self._r(client, "/ddapi/users")
        self.assertEqual([], rv.json)
        self._r(client, "/ddapi/username/u1/u2", method="PUT", response_code=404)
        self._feedInitialData()
        rv = self._r(client, "/ddapi/users")
        self.assertIn("u1", [u["id"] for u in rv.json])
        self._r(client, "/ddapi/username/u1/u2", method="PUT", response_code=501)

    # /ddapi/group
    # /ddapi/group/<group_id>
    def test_group_401(self, client: FlaskClient) -> None:
        for m in ["POST"]:
            self._ur(client, "/ddapi/group", method=m)

    def test_group_405(self, client: FlaskClient) -> None:
        for m in ["GET", "PUT", "DELETE"]:
            self._ur(client, "/ddapi/group", method=m, response_code=405)
            self._r(client, "/ddapi/group", method=m, response_code=405)

    def test_group_POST(self, client: FlaskClient) -> None:
        self._r(client, "/ddapi/group", method="POST", response_code=400)
        for bad_j in self._bad_json:
            self._r(
                client, "/ddapi/group", method="POST", response_code=400, json=bad_j
            )
        rv = self._r(client, "/ddapi/groups")
        self.assertEqual([], rv.json)
        self._r(client, "/ddapi/group", method="POST", json={"name": "group2"})
        rv = self._r(client, "/ddapi/groups")
        self.assertIn("group2", [g["name"] for g in rv.json])
        # TODO: Check what this is supposed to do
        # self._r(client, "/ddapi/group", method='POST', response_code=409,
        #   json={"name": "group2"})

    def test_group_gid_401(self, client: FlaskClient) -> None:
        for m in ["GET", "POST", "DELETE"]:
            self._ur(client, "/ddapi/group/group2", method=m)

    def test_group_gid_405(self, client: FlaskClient) -> None:
        for m in ["PUT"]:
            self._ur(client, "/ddapi/group/group2", method=m, response_code=405)
            self._r(client, "/ddapi/group/group2", method=m, response_code=405)

    def test_group_gid_GET(self, client: FlaskClient) -> None:
        self._r(client, "/ddapi/group/group2", response_code=404)
        self._r(client, "/ddapi/group", method="POST", json={"name": "group2"})
        rv = self._r(client, "/ddapi/group/group2")
        self.assertEqual("group2", rv.json["name"])

    def test_group_gid_POST(self, client: FlaskClient) -> None:
        self._r(client, "/ddapi/group/group2", method="POST", response_code=400)
        for bad_j in self._bad_json:
            self._r(
                client,
                "/ddapi/group/group2",
                method="POST",
                response_code=400,
                json=bad_j,
            )
        self._r(client, "/ddapi/group/group2", response_code=404)
        self._r(client, "/ddapi/group/group2", method="POST", json={"name": "group2"})
        rv = self._r(client, "/ddapi/groups")
        self.assertIn("group2", [g["name"] for g in rv.json])
        rv = self._r(client, "/ddapi/group/group2")
        self.assertEqual("group2", rv.json["name"])
        self._r(
            client,
            "/ddapi/group/group2",
            method="POST",
            response_code=409,
            json={"name": "group2"},
        )

    def test_group_gid_DELETE(self, client: FlaskClient) -> None:
        rv = self._r(client, "/ddapi/groups")
        self.assertEqual([], rv.json)
        self._r(client, "/ddapi/group/group2", method="DELETE", response_code=404)
        self._r(client, "/ddapi/group/group2", method="POST", json={"name": "group2"})
        rv = self._r(client, "/ddapi/groups")
        self.assertIn("group2", [g["name"] for g in rv.json])
        self._r(client, "/ddapi/group/group2", method="DELETE", response_code=200)
        rv = self._r(client, "/ddapi/groups")
        self.assertEqual([], rv.json)

    # /ddapi/user_mail
    # /ddapi/user_mail/<id>
    def test_user_mail_401(self, client: FlaskClient) -> None:
        for m in ["POST"]:
            self._ur(client, "/ddapi/user_mail", method=m)

    def test_user_mail_405(self, client: FlaskClient) -> None:
        for m in ["GET", "DELETE", "PUT"]:
            self._ur(client, "/ddapi/user_mail", method=m, response_code=405)
            self._r(client, "/ddapi/user_mail", method=m, response_code=405)

    def test_user_mail_POST(self, client: FlaskClient) -> None:
        self._r(client, "/ddapi/user_mail", method="POST", response_code=400)
        for bad_j in self._bad_json:
            self._r(
                client, "/ddapi/user_mail", method="POST", response_code=400, json=bad_j
            )
        us = [{"user_id": "u1", "email": "a"}]
        self._r(client, "/ddapi/user_mail", method="POST", response_code=400, json=us)

    def test_user_mail_id_401(self, client: FlaskClient) -> None:
        for m in ["GET", "DELETE"]:
            self._ur(client, "/ddapi/user_mail/id", method=m)

    def test_user_mail_id_405(self, client: FlaskClient) -> None:
        for m in ["POST", "PUT"]:
            self._ur(client, "/ddapi/user_mail/id", method=m, response_code=405)
            self._r(client, "/ddapi/user_mail/id", method=m, response_code=405)

    def test_user_mail_id_GET(self, client: FlaskClient) -> None:
        self._r(client, "/ddapi/user_mail/u1", response_code=501)

    def test_user_mail_id_DELETE(self, client: FlaskClient) -> None:
        self._r(client, "/ddapi/user_mail/u1", method="DELETE", response_code=501)

    # /ddapi/group/users
    #   --> deleted code, was never used
    # def test_group_users_GET_unauth(self, client: FlaskClient) -> None:
    #    return self._ur(
    #        client, "/ddapi/group/users"
    #    )
    # def test_group_users_GET(self, client: FlaskClient) -> None:
    #    rv = self._r(client, "/ddapi/group/users", response_code=405)

    # def test_get_role_users(self, client: FlaskClient) -> None:
    #    rv = self._r(client, "/ddapi/role/users", method="POST")
    #    print(rv)
    #    print(rv.json)


if __name__ == "__main__":
    import sys

    if "DOMAIN" not in os.environ:
        os.environ["DOMAIN"] = "localhost"
    os.environ["SWAGGER_UI"] = "TRUE"
    app = _testApp()
    if "--generate-spec" in sys.argv:
        with app.app_context():
            ep = app.swag.config["specs"][0]["endpoint"]
            spec = app.swag.get_apispecs(ep)
            import json

            print(json.dumps(spec, indent=4))
    else:
        # Start a simple testing server
        app.socketio.run(
            app,
            host=os.environ.get("ADMIN_LISTEN_ADDESS", "::"),
            port=int(os.environ.get("ADMIN_LISTEN_PORT", "9000")),
            debug=True,
        )
