import json
import typing as t

import pkg_resources
import pytest
import requests
from jsonschema import validate
from jupyterlab_code_formatter.formatters import SERVER_FORMATTERS
from jupyterlab_code_formatter.handlers import setup_handlers
from notebook.tests.launchnotebook import NotebookTestBase


def _generate_list_formaters_entry_json_schema(
    formatter_name: str,
) -> t.Dict[str, t.Any]:
    return {
        "type": "object",
        "properties": {
            formatter_name: {
                "type": "object",
                "properties": {
                    "enabled": {"type": "boolean"},
                    "label": {"type": "string"},
                },
            }
        },
    }


EXPECTED_VERSION_SCHEMA = {
    "type": "object",
    "properties": {"version": {"type": "string",}},
}

EXPECTED_LIST_FORMATTERS_SCHEMA = {
    "type": "object",
    "properties": {
        "formatters": {
            formatter_name: _generate_list_formaters_entry_json_schema(formatter_name)
            for formatter_name in SERVER_FORMATTERS
        }
    },
}
EXPECTED_FROMAT_SCHEMA = {
    "type": "object",
    "properties": {
        "code": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {"code": {"type": "string"}, "error": {"type": "string"}},
            },
        }
    },
}


SIMPLE_VALID_PYTHON_CODE = "x= 22;  e          =1"


class TestHandlers(NotebookTestBase):
    def setUp(self) -> None:
        setup_handlers(self.notebook.web_app)

    def _create_headers(
        self, plugin_version: t.Optional[str] = None
    ) -> t.Dict[str, str]:
        return {
            "Plugin-Version": plugin_version
            if plugin_version is not None
            else pkg_resources.get_distribution("jupyterlab_code_formatter").version
        }

    def _format_code_request(
        self,
        formatter: str,
        code: t.List[str],
        options: t.Dict[str, t.Any],
        plugin_version: t.Optional[str] = None,
    ) -> requests.Response:
        return self.request(
            verb="POST",
            path="/jupyterlab_code_formatter/format",
            data=json.dumps(
                {
                    "code": code,
                    "options": options,
                    "notebook": True,
                    "formatter": formatter,
                }
            ),
            headers=self._create_headers(plugin_version),
        )

    @staticmethod
    def _check_http_200_and_schema(response):
        assert response.status_code == 200
        json_result = response.json()
        validate(instance=json_result, schema=EXPECTED_FROMAT_SCHEMA)
        return json_result

    def test_list_formatters(self):
        """Check if the formatters list route works."""
        response = self.request(
            verb="GET",
            path="/jupyterlab_code_formatter/formatters",
            headers=self._create_headers(),
        )
        validate(instance=response.json(), schema=EXPECTED_LIST_FORMATTERS_SCHEMA)

    def test_404_on_unknown(self):
        """Check that it 404 correctly if formatter name is bad."""
        response = self._format_code_request(
            formatter="UNKNOWN", code=[SIMPLE_VALID_PYTHON_CODE], options={}
        )
        assert response.status_code == 404

    def test_can_apply_python_formatter(self):
        """Check that it can apply black with simple config."""
        response = self._format_code_request(
            formatter="black",
            code=[SIMPLE_VALID_PYTHON_CODE],
            options={"line_length": 88},
        )
        json_result = self._check_http_200_and_schema(response)
        assert json_result["code"][0]["code"] == "x = 22\ne = 1"

    def test_can_use_black_config(self):
        """Check that it can apply black with advanced config."""
        given = "some_string='abc'"
        expected = "some_string = 'abc'"

        response = self._format_code_request(
            formatter="black",
            options={"line_length": 123, "string_normalization": False},
            code=[given],
        )
        json_result = self._check_http_200_and_schema(response)
        assert json_result["code"][0]["code"] == expected

    def test_return_error_if_any(self):
        """Check that it returns the error if any."""
        bad_python = "this_is_bad = 'hihi"
        response = self._format_code_request(
            formatter="black",
            options={"line_length": 123, "string_normalization": False},
            code=[bad_python],
        )
        json_result = self._check_http_200_and_schema(response)
        assert (
            json_result["code"][0]["error"] == "Cannot parse: 1:13: this_is_bad = 'hihi"
        )

    def test_can_handle_magic(self):
        """Check that it's fine to run formatters for code with magic."""
        given = '%%timeit\nsome_string = "abc"'
        expected = '%%timeit\nsome_string = "abc"'
        for formatter in ["black", "yapf", "isort"]:
            response = self._format_code_request(
                formatter=formatter, code=[given], options={},
            )
            json_result = self._check_http_200_and_schema(response)
            assert json_result["code"][0]["code"] == expected

    def test_can_use_styler(self):
        given = "a = 3; 2"
        expected = "a <- 3\n2"
        response = self._format_code_request(
            formatter="styler", code=[given], options={"scope": "tokens"},
        )
        json_result = self._check_http_200_and_schema(response)
        assert json_result["code"][0]["code"] == expected

    def test_can_use_styler_2(self):
        given = """data_frame(
     small  = 2 ,
     medium = 4,#comment without space
     large  =6
)"""
        expected = """data_frame(
  small  = 2,
  medium = 4, # comment without space
  large  = 6
)"""
        response = self._format_code_request(
            code=[given], options={"strict": False}, formatter="styler",
        )
        json_result = self._check_http_200_and_schema(response)
        assert json_result["code"][0]["code"] == expected

    def test_can_use_styler_3(self):
        given = "1++1/2*2^2"
        expected = "1 + +1/2*2^2"
        response = self._format_code_request(
            formatter="styler",
            options={
                "math_token_spacing": {
                    "one": ["'+'", "'-'"],
                    "zero": ["'/'", "'*'", "'^'"],
                }
            },
            code=[given],
        )
        json_result = self._check_http_200_and_schema(response)
        assert json_result["code"][0]["code"] == expected

    def test_can_use_styler_4(self):
        given = """a <- function() {
    ### not to be indented
    # indent normally
    33
    }"""
        expected = """a <- function() {
### not to be indented
  # indent normally
  33
}"""

        response = self._format_code_request(
            code=[given],
            formatter="styler",
            options=dict(
                reindention=dict(regex_pattern="^###", indention=0, comments_only=True)
            ),
        )
        json_result = self._check_http_200_and_schema(response)
        assert json_result["code"][0]["code"] == expected

    def test_can_use_styler_5(self):
        given = """call(
#          SHOULD BE ONE SPACE BEFORE
1,2)
"""
        expected = """call(
    # SHOULD BE ONE SPACE BEFORE
    1, 2
)"""
        response = self._format_code_request(
            code=[given],
            formatter="styler",
            options=dict(indent_by=4, start_comments_with_one_space=True),
        )
        json_result = self._check_http_200_and_schema(response)
        assert json_result["code"][0]["code"] == expected

    def test_can_use_styler_6(self):
        given = "1+1-3"
        expected = "1 + 1 - 3"

        response = self._format_code_request(
            code=[given],
            formatter="styler",
            options={
                "math_token_spacing": "tidyverse_math_token_spacing",
                "reindention": "tidyverse_reindention",
            },
        )
        json_result = self._check_http_200_and_schema(response)
        assert json_result["code"][0]["code"] == expected

    def test_422_on_mismatch_version_1(self):
        response = self.request(
            verb="GET",
            path="/jupyterlab_code_formatter/formatters",
            headers=self._create_headers("0.0.0"),
        )
        assert response.status_code == 422

    def test_200_on_version_without_header(self):
        response = self.request(verb="GET", path="/jupyterlab_code_formatter/version",)
        assert response.status_code == 200
        validate(instance=response.json(), schema=EXPECTED_VERSION_SCHEMA)

    def test_200_on_version_with_wrong_header(self):
        response = self.request(
            verb="GET",
            path="/jupyterlab_code_formatter/version",
            headers=self._create_headers("0.0.0"),
        )
        assert response.status_code == 200
        validate(instance=response.json(), schema=EXPECTED_VERSION_SCHEMA)

    def test_200_on_version_with_correct_header(self):
        response = self.request(
            verb="GET",
            path="/jupyterlab_code_formatter/version",
            headers=self._create_headers(),
        )
        assert response.status_code == 200
        validate(instance=response.json(), schema=EXPECTED_VERSION_SCHEMA)