from __future__ import annotations

import sys
import unittest
from pathlib import Path
from types import SimpleNamespace

ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / "src"
if str(SRC) not in sys.path:
    sys.path.insert(0, str(SRC))

from brightree.exceptions import BrightreeException
from brightree.services import CustomField, Doctor, Patient, SalesOrder, Security
from brightree.services.base import default_config


class SoapFault(Exception):
    def __init__(self, message: str, code: int, detail: dict) -> None:
        super().__init__(message)
        self.message = message
        self.code = code
        self.detail = detail


class FakeSoapClient:
    def __init__(self, **kwargs) -> None:
        self.kwargs = kwargs

    def call(self, method, params):
        return {"method": method, "params": params}


class FaultingSoapClient(FakeSoapClient):
    def call(self, method, params):
        raise SoapFault("Remote failure", 503, {"method": method})


def info_for(service_name: str) -> dict:
    return {
        "username": "testuser",
        "password": "testpass",
        "config": {"service": {service_name: f"https://example.com/{service_name}"}},
    }


class ServiceTest(unittest.TestCase):
    def test_default_config_contains_known_services(self) -> None:
        config = default_config()
        self.assertIn("service", config)
        self.assertIn("patient", config["service"])
        self.assertIn("doctor", config["service"])

    def test_constructor_requires_credentials(self) -> None:
        with self.assertRaises(BrightreeException) as ctx:
            CustomField({"config": {"service": {"custom": "https://example.com/custom"}}})
        self.assertEqual(ctx.exception.code, 1001)
        self.assertIn("Authentication credentials not provided", str(ctx.exception))

    def test_custom_field_fetch_by_category(self) -> None:
        service = CustomField(info_for("custom"), client_factory=FakeSoapClient)
        result = service.CustomFieldFetchAllByCategory("Patient", 1)
        self.assertEqual(
            result,
            {
                "method": "CustomFieldFetchAllByCategory",
                "params": {"category": "Patient", "includeInactive": 1},
            },
        )

    def test_magic_call_with_standard_query_method(self) -> None:
        service = Doctor(info_for("doctor"), client_factory=FakeSoapClient)
        result = service.DoctorSearch({"lastName": "Smith"})
        self.assertEqual(result["method"], "DoctorSearch")
        self.assertEqual(result["params"], {"lastName": "Smith"})

    def test_magic_call_with_special_method(self) -> None:
        service = Doctor(info_for("doctor"), client_factory=FakeSoapClient)
        result = service.DoctorFetchByExternalID(12345)
        self.assertEqual(result["params"], {"ExternalID": 12345})

    def test_magic_call_without_query_uses_empty_payload(self) -> None:
        service = Security(info_for("security"), client_factory=FakeSoapClient)
        result = service.UserGroupFetchAll()
        self.assertEqual(result["method"], "UserGroupFetchAll")
        self.assertEqual(result["params"], {})

    def test_missing_special_method_argument_raises_param_error(self) -> None:
        service = Doctor(info_for("doctor"), client_factory=FakeSoapClient)
        with self.assertRaises(BrightreeException) as ctx:
            service.DoctorFetchByExternalID()
        self.assertEqual(ctx.exception.code, 1002)
        self.assertIn("ExternalID", str(ctx.exception))

    def test_non_existent_method_raises_attribute_error(self) -> None:
        service = Doctor(info_for("doctor"), client_factory=FakeSoapClient)
        with self.assertRaises(AttributeError):
            service.NonExistentMethod()

    def test_patient_search_requires_iterable(self) -> None:
        service = Patient(info_for("patient"), client_factory=FakeSoapClient)
        with self.assertRaises(BrightreeException) as ctx:
            service.PatientSearch("not-an-array")
        self.assertEqual(ctx.exception.code, 1002)
        self.assertIn("Method PatientSearch requires an iterable parameter", str(ctx.exception))

    def test_patient_fetch_validates_brightree_id(self) -> None:
        service = Patient(info_for("patient"), client_factory=FakeSoapClient)
        with self.assertRaises(BrightreeException) as ctx:
            service.PatientFetchByBrightreeID(0)
        self.assertEqual(ctx.exception.code, 1003)
        self.assertIn("Invalid Brightree ID: 0", str(ctx.exception))

    def test_sales_order_fetch_requires_brightree_id_field(self) -> None:
        service = SalesOrder(info_for("salesorder"), client_factory=FakeSoapClient)
        with self.assertRaises(BrightreeException) as ctx:
            service.SalesOrderFetchByBrightreeID({})
        self.assertEqual(ctx.exception.code, 1003)
        self.assertIn("BrightreeID is required for SalesOrderFetchByBrightreeID", str(ctx.exception))

    def test_soap_faults_are_wrapped_with_request_context(self) -> None:
        service = Doctor(info_for("doctor"), client_factory=FaultingSoapClient)
        with self.assertRaises(BrightreeException) as ctx:
            service.DoctorSearch({"lastName": "Smith"})

        self.assertEqual(ctx.exception.code, 503)
        self.assertEqual(str(ctx.exception), "Remote failure")
        self.assertEqual(
            ctx.exception.request_data,
            {"method": "DoctorSearch", "params": {"lastName": "Smith"}},
        )
        self.assertEqual(ctx.exception.error_data, {"method": "DoctorSearch"})

    def test_api_response_exception_extracts_nested_error(self) -> None:
        response = SimpleNamespace(Error=SimpleNamespace(Message="Bad payload", Code=422))
        exception = BrightreeException.from_api_response(response, request_data={"method": "DoctorCreate"})
        self.assertEqual(str(exception), "Bad payload")
        self.assertEqual(exception.code, 422)
        self.assertIs(exception.error_data, response)
        self.assertEqual(exception.request_data, {"method": "DoctorCreate"})


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