from typing import Dict, List, Optional, Union

from pydantic import BaseModel


class FundingSourceConfig(BaseModel):
    name: str
    skip: Optional[bool]
    wallet_class: str
    settings: dict
    mock_settings: Optional[dict]


class FunctionMock(BaseModel):
    uri: Optional[str]
    query_params: Optional[dict]
    headers: Optional[dict]
    method: Optional[str]


class TestMock(BaseModel):
    skip: Optional[bool]
    description: Optional[str]
    request_type: Optional[str]
    request_body: Optional[dict]
    response_type: str
    response: Union[str, dict, list]


class Mock(FunctionMock, TestMock):
    name: str

    @staticmethod
    def combine_mocks(mock_name, fs_mock, test_mock):
        _mock = fs_mock | test_mock
        if "response" in _mock and "response" in fs_mock:
            _mock["response"] |= fs_mock["response"]
        m = Mock(name=mock_name, **_mock)

        return m


class FunctionMocks(BaseModel):
    mocks: Dict[str, FunctionMock]


class FunctionTest(BaseModel):
    description: str
    call_params: dict
    expect: dict
    mocks: Dict[str, List[Dict[str, TestMock]]]


class FunctionData(BaseModel):
    """Data required for testing this function"""

    "Function level mocks that apply for all tests of this function"
    mocks: List[FunctionMock] = []

    "All the tests for this function"
    tests: List[FunctionTest] = []


class WalletTest(BaseModel):
    skip: Optional[bool]
    function: str
    description: str
    funding_source: FundingSourceConfig
    call_params: Optional[dict] = {}
    expect: Optional[dict]
    expect_error: Optional[dict]
    mocks: List[Mock] = []

    @staticmethod
    def tests_for_funding_source(
        fs: FundingSourceConfig,
        fn_name: str,
        fn,
        test,
    ) -> List["WalletTest"]:
        t = WalletTest(
            **{
                "funding_source": fs,
                "function": fn_name,
                **test,
                "mocks": [],
                "skip": fs.skip,
            }
        )
        if "mocks" in test:
            if fs.name not in test["mocks"]:
                t.skip = True
                return [t]

            return t._tests_from_fs_mocks(fn, test, fs.name)

        return [t]

    def _tests_from_fs_mocks(self, fn, test, fs_name: str) -> List["WalletTest"]:

        fs_mocks = fn["mocks"][fs_name]
        test_mocks = test["mocks"][fs_name]

        all_test_mocks = [test_mocks] if isinstance(test_mocks, dict) else test_mocks

        mocks = []
        for tm in all_test_mocks:
            mocks += self._build_mock_objects(list(fs_mocks), fs_mocks, tm)

        return [self._tests_from_mock(m) for m in mocks]

    def _build_mock_objects(self, mock_names, fs_mocks, test_mocks):
        mocks = []

        for mock_name in mock_names:
            if mock_name not in test_mocks:
                continue
            for test_mock in test_mocks[mock_name]:
                mock = {"fs_mock": fs_mocks[mock_name], "test_mock": test_mock}

                if len(mock_names) == 1:
                    mocks.append({mock_name: mock})
                else:
                    sub_mocks = self._build_mock_objects(
                        mock_names[1:], fs_mocks, test_mocks
                    )
                    for sub_mock in sub_mocks:
                        mocks.append({mock_name: mock} | sub_mock)
            return mocks

        return mocks

    def _tests_from_mock(self, mock_obj) -> "WalletTest":

        test_mocks: List[Mock] = [
            Mock.combine_mocks(
                mock_name,
                mock_obj[mock_name]["fs_mock"],
                mock_obj[mock_name]["test_mock"],
            )
            for mock_name in mock_obj
        ]

        any_mock_skipped = len([m for m in test_mocks if m.skip])
        extra_description = ";".join(
            [m.description for m in test_mocks if m.description]
        )

        return WalletTest(
            **(
                self.dict()
                | {
                    "description": f"{self.description}:{extra_description}",
                    "mocks": test_mocks,
                    "skip": self.skip or any_mock_skipped,
                }
            )
        )


class DataObject:
    def __init__(self, **kwargs):
        for k in kwargs:
            setattr(self, k, kwargs[k])

    def __str__(self):
        data = []
        for k in self.__dict__:
            value = getattr(self, k)
            if isinstance(value, list):
                value = [f"{k}={v}" for v in value]
            data.append(f"{k}={value}")
        return ";".join(data)
