import importlib
import json
import logging
import os
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import Mock, patch

PROJECT_ROOT = Path(__file__).resolve().parent.parent
SRC_ROOT = PROJECT_ROOT / "src"
if str(SRC_ROOT) not in sys.path:
    sys.path.insert(0, str(SRC_ROOT))


class Request24Tests(unittest.TestCase):
    def setUp(self):
        self.env_patcher = patch.dict(os.environ, {}, clear=True)
        self.env_patcher.start()

    def tearDown(self):
        self.env_patcher.stop()

    def _reload_rest_module(self):
        import a24wh.config
        import a24wh.chat
        import a24wh.logging_utils
        import a24wh.rest

        importlib.reload(a24wh.config)
        importlib.reload(a24wh.chat)
        importlib.reload(a24wh.logging_utils)
        return importlib.reload(a24wh.rest)

    def _reload_chat_module(self):
        import a24wh.chat

        return importlib.reload(a24wh.chat)

    def test_uses_default_webhook_from_env(self):
        os.environ["A24WH_DEFAULT_WEBHOOK_URL"] = "https://example.bitrix24.ru/rest/1/token/"
        rest = self._reload_rest_module()
        response_mock = Mock()
        response_mock.status_code = 200
        response_mock.json.return_value = {"result": [{"ID": 1}]}

        with patch.object(rest.requests, "post", return_value=response_mock) as post_mock:
            result = rest.request24("user.get")

        self.assertEqual(result, {"result": [{"ID": 1}]})
        post_mock.assert_called_once_with(
            "https://example.bitrix24.ru/rest/1/token/user.get.json",
            timeout=60,
            )

    def test_explicit_webhook_overrides_default(self):
        os.environ["A24WH_DEFAULT_WEBHOOK_URL"] = "https://default.bitrix24.ru/rest/1/default/"
        rest = self._reload_rest_module()
        response_mock = Mock()
        response_mock.status_code = 200
        response_mock.json.return_value = {"result": True}

        with patch.object(rest.requests, "post", return_value=response_mock) as post_mock:
            result = rest.request24(
                "crm.contact.get",
                {"id": 5},
                "https://custom.bitrix24.ru/rest/1/custom/",
                )

        self.assertEqual(result, {"result": True})
        post_mock.assert_called_once_with(
            "https://custom.bitrix24.ru/rest/1/custom/crm.contact.get.json",
            json={"id": 5},
            timeout=60,
            )

    def test_req24_uses_rest_v3_url_and_post_json(self):
        os.environ["A24WH_DEFAULT_WEBHOOK_URL"] = "https://example.bitrix24.ru/rest/1/token/"
        rest = self._reload_rest_module()
        response_mock = Mock()
        response_mock.status_code = 200
        response_mock.json.return_value = {"result": {"item": {"id": 5}}}

        with patch.object(rest.requests, "post", return_value=response_mock) as post_mock:
            result = rest.req24("crm.item.get", {"id": 5})

        self.assertEqual(result, {"result": {"item": {"id": 5}}})
        post_mock.assert_called_once_with(
            "https://example.bitrix24.ru/rest/api/1/token/crm.item.get",
            json={"id": 5},
            headers={
                "Content-Type": "application/json",
                "Accept": "application/json",
                },
            timeout=60,
            )

    def test_req24_uses_get_without_parameters(self):
        os.environ["A24WH_DEFAULT_WEBHOOK_URL"] = "https://example.bitrix24.ru/rest/1/token/"
        rest = self._reload_rest_module()
        response_mock = Mock()
        response_mock.status_code = 200
        response_mock.json.return_value = {"result": {"items": []}}

        with patch.object(rest.requests, "get", return_value=response_mock) as get_mock:
            result = rest.req24("crm.item.list")

        self.assertEqual(result, {"result": {"items": []}})
        get_mock.assert_called_once_with(
            "https://example.bitrix24.ru/rest/api/1/token/crm.item.list",
            headers={
                "Content-Type": "application/json",
                "Accept": "application/json",
                },
            timeout=60,
            )

    def test_req24_wraps_v3_api_error(self):
        os.environ["A24WH_DEFAULT_WEBHOOK_URL"] = "https://example.bitrix24.ru/rest/1/token/"
        rest = self._reload_rest_module()
        response_mock = Mock()
        response_mock.status_code = 200
        response_mock.json.return_value = {
            "error": {
                "code": "ERROR_CORE",
                "message": "Something failed",
                }
            }

        with patch.object(rest.requests, "get", return_value=response_mock):
            result = rest.req24("crm.item.list")

        self.assertTrue(result["error_result"])
        self.assertEqual(result["error_type"], "bitrix_error")
        self.assertEqual(result["error_code"], "ERROR_CORE")
        self.assertEqual(result["error_message"], "Something failed")

    def test_returns_error_dict_when_webhook_missing(self):
        rest = self._reload_rest_module()

        result = rest.request24("user.get")

        self.assertIn("error_result", result)
        self.assertEqual(result["error_code"], "missing_webhook_url")

    def test_wraps_bitrix_business_error(self):
        os.environ["A24WH_DEFAULT_WEBHOOK_URL"] = "https://example.bitrix24.ru/rest/1/token/"
        rest = self._reload_rest_module()
        response_mock = Mock()
        response_mock.status_code = 200
        response_mock.json.return_value = {
            "error": "ERROR_METHOD_NOT_FOUND",
            "error_description": "Method not found!",
            }

        with patch.object(rest.requests, "post", return_value=response_mock):
            result = rest.request24("bad.method")

        self.assertTrue(result["error_result"])
        self.assertEqual(result["error_type"], "bitrix_error")
        self.assertEqual(result["error_code"], "ERROR_METHOD_NOT_FOUND")

    def test_returns_transport_error_dict(self):
        os.environ["A24WH_DEFAULT_WEBHOOK_URL"] = "https://example.bitrix24.ru/rest/1/token/"
        rest = self._reload_rest_module()

        with patch.object(rest.requests, "post", side_effect=rest.requests.ConnectionError("network down")):
            result = rest.request24("user.get")

        self.assertTrue(result["error_result"])
        self.assertEqual(result["error_type"], "transport_error")

    def test_to_chat_calls_im_message_add(self):
        chat_module = self._reload_chat_module()

        with patch.object(chat_module, "request24", return_value={"result": 101}) as request_mock:
            result = chat_module.to_chat(
                text="hello",
                chat_id="chat11111",
                webhook_url="https://custom.bitrix24.ru/rest/1/custom/",
                )

        self.assertEqual(result, {"result": 101})
        request_mock.assert_called_once_with(
            method="im.message.add",
            parameters={
                "DIALOG_ID": "chat11111",
                "MESSAGE": "hello",
                "SYSTEM": "N",
                },
            webhook_url="https://custom.bitrix24.ru/rest/1/custom/",
            )

    def test_to_chat_validates_chat_id(self):
        chat_module = self._reload_chat_module()

        with self.assertRaises(ValueError):
            chat_module.to_chat(text="hello", chat_id="  ")

        with self.assertRaises(ValueError):
            chat_module.to_chat(text="hello", chat_id="11111")

        with self.assertRaises(ValueError):
            chat_module.to_chat(text="hello", chat_id="chatabc")

    def test_to_chat_validates_text(self):
        chat_module = self._reload_chat_module()

        with self.assertRaises(ValueError):
            chat_module.to_chat(text="", chat_id="chat11111")

        with self.assertRaises(ValueError):
            chat_module.to_chat(text="   ", chat_id="chat11111")

    def test_auto_logging_can_be_enabled_from_env(self):
        os.environ["A24WH_AUTO_CONFIGURE_LOGGING"] = "1"
        os.environ["A24WH_LOG_TO_CONSOLE"] = "0"
        os.environ["A24WH_LOG_TO_FILE"] = "0"

        import a24wh.logging_utils

        logging_utils = importlib.reload(a24wh.logging_utils)
        logger = logging_utils.auto_configure_logging()

        self.assertEqual(logger.name, "a24wh")
        self.assertEqual(logger.level, logging.INFO)

    def test_save_json_uses_default_file_name_from_response_metadata(self):
        import a24wh.utils

        utils = importlib.reload(a24wh.utils)
        response_data = {
            "result": {"ID": 10},
            }

        with tempfile.TemporaryDirectory() as tmp_dir:
            current_dir = Path.cwd()
            os.chdir(tmp_dir)
            try:
                saved_path = utils.save_json(response_data)
                saved_file = Path(saved_path)
                self.assertEqual(saved_file.name, "a24wh_request24.json")
                self.assertTrue(saved_file.exists())
                with saved_file.open("r", encoding="utf-8") as file_obj:
                    loaded = json.load(file_obj)
                self.assertEqual(loaded["result"]["ID"], 10)
            finally:
                os.chdir(current_dir)

    def test_payload_preview_keeps_regular_values(self):
        rest = self._reload_rest_module()

        preview = rest._safe_payload_preview({
            "id": 58262,
            "entityTypeId": 2,
            "filter": {
                "=ID": [58262, 58263],
                },
            })

        self.assertEqual(preview["id"], 58262)
        self.assertEqual(preview["entityTypeId"], 2)
        self.assertEqual(preview["filter"]["=ID"], [58262, 58263])

    def test_payload_preview_masks_sensitive_values(self):
        rest = self._reload_rest_module()

        preview = rest._safe_payload_preview({
            "id": 58262,
            "phone": "+79991234567",
            "EMAIL": "test@example.com",
            "access_token": "very-secret-token",
            "fields": {
                "PASSWORD": "qwerty",
                "UF_CRM_PHONE": "+78889990000",
                "NAME": "Ivan",
                },
            })

        self.assertEqual(preview["id"], 58262)
        self.assertEqual(preview["phone"], "***")
        self.assertEqual(preview["EMAIL"], "***")
        self.assertEqual(preview["access_token"], "***")
        self.assertEqual(preview["fields"]["PASSWORD"], "***")
        self.assertEqual(preview["fields"]["UF_CRM_PHONE"], "***")
        self.assertEqual(preview["fields"]["NAME"], "Ivan")

    def test_all_iters_collects_all_pages(self):
        os.environ["A24WH_DEFAULT_WEBHOOK_URL"] = "https://example.bitrix24.ru/rest/1/token/"
        os.environ["A24WH_REQUEST_DELAY"] = "0"
        rest = self._reload_rest_module()

        first_response = Mock()
        first_response.status_code = 200
        first_response.json.return_value = {
            "result": {
                "items": [{"ID": 1}, {"ID": 2}],
                },
            "next": 50,
            "total": 4,
            }

        second_response = Mock()
        second_response.status_code = 200
        second_response.json.return_value = {
            "result": {
                "items": [{"ID": 3}, {"ID": 4}],
                },
            "total": 4,
            }

        with patch.object(rest.requests, "post", side_effect=[first_response, second_response]) as post_mock:
            result = rest.request24("crm.deal.list", {"filter": {"=CATEGORY_ID": 1}}, all_iters=True)

        self.assertEqual(result["result"]["items"], [{"ID": 1}, {"ID": 2}, {"ID": 3}, {"ID": 4}])
        self.assertEqual(result["total"], 4)
        self.assertNotIn("next", result)
        self.assertEqual(post_mock.call_count, 2)

    def test_all_iters_collects_all_pages_for_root_result_list(self):
        os.environ["A24WH_DEFAULT_WEBHOOK_URL"] = "https://example.bitrix24.ru/rest/1/token/"
        os.environ["A24WH_REQUEST_DELAY"] = "0"
        rest = self._reload_rest_module()

        first_response = Mock()
        first_response.status_code = 200
        first_response.json.return_value = {
            "result": [{"ID": 1}, {"ID": 2}],
            "next": 50,
            "total": 4,
            }

        second_response = Mock()
        second_response.status_code = 200
        second_response.json.return_value = {
            "result": [{"ID": 3}, {"ID": 4}],
            "total": 4,
            }

        with patch.object(rest.requests, "post", side_effect=[first_response, second_response]) as post_mock:
            result = rest.request24("crm.deal.list", {"filter": {"=CATEGORY_ID": 1}}, all_iters=True)

        self.assertEqual(result["result"], [{"ID": 1}, {"ID": 2}, {"ID": 3}, {"ID": 4}])
        self.assertEqual(result["total"], 4)
        self.assertNotIn("next", result)
        self.assertEqual(post_mock.call_count, 2)

    def test_config_reads_request_delay_from_env(self):
        os.environ["A24WH_REQUEST_DELAY"] = "0.3"

        import a24wh.config

        config_module = importlib.reload(a24wh.config)
        config = config_module.get_config()

        self.assertEqual(config.request_delay, 0.3)

    def test_internal_batch_request_calls_request24_batch(self):
        import a24wh.batch

        batch_module = importlib.reload(a24wh.batch)
        with patch.object(batch_module, "request24", return_value={"result": {"result": {}}}) as request_mock:
            result = batch_module._batch_request({"step_0": "crm.deal.get?id=1"})

        self.assertEqual(result, {"result": {"result": {}}})
        request_mock.assert_called_once_with(
            "batch",
            {"halt": 1, "cmd": {"step_0": "crm.deal.get?id=1"}},
            None,
            timeout=None,
            )

    def test_batch_by_params_aggregates_step_results(self):
        import a24wh.batch

        batch_module = importlib.reload(a24wh.batch)
        batch_response = {
            "result": {
                "result": {
                    "step_0": {"ID": "1", "TITLE": "Deal 1"},
                    "step_1": {"ID": "2", "TITLE": "Deal 2"},
                    },
                "result_error": {},
                }
            }

        with patch.object(batch_module, "_batch_request", return_value=batch_response):
            result = batch_module.batch_by_params(
                "crm.deal.get",
                [{"id": 1}, {"id": 2}],
                )

        self.assertEqual(
            result,
            {
                "result": [
                    {"ID": "1", "TITLE": "Deal 1"},
                    {"ID": "2", "TITLE": "Deal 2"},
                    ],
                "total": 2,
                },
            )

    def test_batch_by_params_validates_items_are_dicts(self):
        import a24wh.batch

        batch_module = importlib.reload(a24wh.batch)
        result = batch_module.batch_by_params("crm.deal.get", [{"id": 1}, 2])

        self.assertTrue(result["error_result"])
        self.assertEqual(result["error_code"], "invalid_params_item")

    def test_batch_list_collects_multiple_batches(self):
        os.environ["A24WH_REQUEST_DELAY"] = "0"
        import a24wh.batch

        batch_module = importlib.reload(a24wh.batch)
        first_batch = {
            "result": {
                "result": {
                    "step_0": [{"ID": 1}, {"ID": 2}],
                    "step_1": [{"ID": 3}, {"ID": 4}],
                    },
                "result_error": {},
                }
            }
        second_batch = {
            "result": {
                "result": {
                    "step_0": [{"ID": 5}],
                    "step_1": [],
                    },
                "result_error": {},
                }
            }

        with patch.object(batch_module, "DEFAULT_BATCH_SIZE", 2), patch.object(batch_module, "DEFAULT_PAGE_SIZE",
                                                                               2), patch.object(batch_module,
                                                                                                "_batch_request",
                                                                                                side_effect=[
                                                                                                    first_batch,
                                                                                                    second_batch]):
            result = batch_module.batch_list("crm.deal.list", {"filter": {"=CATEGORY_ID": 1}})

        self.assertEqual(result["result"], [{"ID": 1}, {"ID": 2}, {"ID": 3}, {"ID": 4}, {"ID": 5}])
        self.assertEqual(result["total"], 5)

    def test_batch_by_params_uses_request_delay_between_chunks(self):
        os.environ["A24WH_REQUEST_DELAY"] = "0.25"
        import a24wh.batch

        batch_module = importlib.reload(a24wh.batch)
        batch_response_1 = {
            "result": {
                "result": {f"step_{index}": {"ID": str(index + 1)} for index in range(50)},
                "result_error": {},
                }
            }
        batch_response_2 = {
            "result": {
                "result": {"step_0": {"ID": "51"}},
                "result_error": {},
                }
            }
        params_list = [{"id": index} for index in range(51)]

        with patch.object(batch_module, "_batch_request",
                          side_effect=[batch_response_1, batch_response_2]), patch.object(batch_module.time,
                                                                                          "sleep") as sleep_mock:
            result = batch_module.batch_by_params("crm.deal.get", params_list)

        self.assertEqual(result["total"], 51)
        sleep_mock.assert_called_once_with(0.25)


if __name__ == "__main__":
    unittest.main()
