from pathlib import Path
from typing import Iterable, Optional

import pytest
from eth_account.account import Account

from nucypher.blockchain.eth.actors import Operator
from nucypher.blockchain.eth.agents import (
    ContractAgency,
    CoordinatorAgent,
    StakingProvidersReservoir,
    TACoApplicationAgent,
    TACoChildApplicationAgent,
)
from nucypher.blockchain.eth.clients import EthereumClient
from nucypher.blockchain.eth.interfaces import (
    BlockchainInterface,
    BlockchainInterfaceFactory,
)
from nucypher.blockchain.eth.registry import (
    ContractRegistry,
)
from nucypher.blockchain.eth.signers import KeystoreSigner
from nucypher.characters.lawful import Ursula
from nucypher.cli.types import ChecksumAddress
from nucypher.config.characters import UrsulaConfiguration
from nucypher.crypto.powers import TransactingPower
from nucypher.network.nodes import Teacher
from nucypher.policy.payment import SubscriptionManagerPayment
from tests.constants import (
    KEYFILE_NAME_TEMPLATE,
    MOCK_KEYSTORE_PATH,
    NUMBER_OF_MOCK_KEYSTORE_ACCOUNTS,
    TEMPORARY_DOMAIN,
    TESTERCHAIN_CHAIN_ID,
)
from tests.mock.agents import MockContractAgency
from tests.mock.interfaces import MockBlockchain
from tests.mock.io import MockStdinWrapper
from tests.utils.registry import MockRegistrySource, mock_registry_sources
from tests.utils.ursula import (
    mock_permitted_multichain_connections,
    setup_multichain_ursulas,
)


def pytest_addhooks(pluginmanager):
    pluginmanager.set_blocked('ape_test')


@pytest.fixture(scope="module", autouse=True)
def mock_sample_reservoir(accounts, mock_contract_agency):
    def mock_reservoir(
        without: Optional[Iterable[ChecksumAddress]] = None, *args, **kwargs
    ):
        addresses = {
            address: 1
            for address in accounts.staking_providers_accounts
            if address not in without
        }
        return StakingProvidersReservoir(addresses)

    mock_agent = mock_contract_agency.get_agent(TACoApplicationAgent)
    mock_agent.get_staking_provider_reservoir = mock_reservoir


@pytest.fixture(scope="function", autouse=True)
def mock_taco_application_agent(testerchain, mock_contract_agency):
    mock_agent = mock_contract_agency.get_agent(TACoApplicationAgent)
    yield mock_agent
    mock_agent.reset()


@pytest.fixture(scope="function", autouse=True)
def mock_taco_child_application_agent(testerchain, mock_contract_agency):
    mock_agent = mock_contract_agency.get_agent(TACoChildApplicationAgent)
    yield mock_agent
    mock_agent.reset()


@pytest.fixture(scope="function", autouse=True)
def mock_coordinator_agent(testerchain, mock_contract_agency):
    from tests.mock.coordinator import MockCoordinatorAgent

    mock_agent = MockCoordinatorAgent(blockchain=testerchain)
    mock_contract_agency._MockContractAgency__agents[CoordinatorAgent] = mock_agent
    yield mock_agent
    mock_agent.reset()


@pytest.fixture(scope='function')
def mock_stdin(mocker):
    mock = MockStdinWrapper()

    mocker.patch('sys.stdin', new=mock.mock_stdin)
    mocker.patch('getpass.getpass', new=mock.mock_getpass)

    yield mock

    # Sanity check.
    # The user is encouraged to `assert mock_stdin.empty()` explicitly in the test
    # right after the input-consuming function call.
    assert mock.empty(), "Stdin mock was not empty on teardown - some unclaimed input remained"


@pytest.fixture(scope="module")
def testerchain(mock_testerchain, module_mocker, clock) -> MockBlockchain:
    def always_use_mock(*a, **k):
        return mock_testerchain

    module_mocker.patch.object(
        BlockchainInterfaceFactory, "get_interface", always_use_mock
    )

    mock_testerchain.tx_machine._task.clock = clock
    return mock_testerchain


@pytest.fixture(scope='module', autouse=True)
def mock_interface(module_mocker):
    # Generic Interface
    mock_transaction_sender = module_mocker.patch.object(BlockchainInterface, 'sign_and_broadcast_transaction')
    mock_transaction_sender.return_value = MockBlockchain.FAKE_RECEIPT
    return mock_transaction_sender


@pytest.fixture(scope='module')
def test_registry(module_mocker):
    with mock_registry_sources(mocker=module_mocker):
        mock_source = MockRegistrySource(domain=TEMPORARY_DOMAIN)
        registry = ContractRegistry(source=mock_source)
        yield registry


@pytest.fixture(scope='module', autouse=True)
def mock_contract_agency():
    # Patch

    # Monkeypatch # TODO: Use better tooling for this monkeypatch?
    get_agent = ContractAgency.get_agent
    get_agent_by_name = ContractAgency.get_agent_by_contract_name
    ContractAgency.get_agent = MockContractAgency.get_agent
    ContractAgency.get_agent_by_contract_name = MockContractAgency.get_agent_by_contract_name

    # Test
    yield MockContractAgency()

    # Restore the monkey patching
    ContractAgency.get_agent = get_agent
    ContractAgency.get_agent_by_contract_name = get_agent_by_name


@pytest.fixture(scope='module')
def agency(mock_contract_agency):
    yield mock_contract_agency


@pytest.fixture(scope="function")
def mock_funding_and_bonding(
    accounts, mocker, mock_taco_application_agent, mock_taco_child_application_agent
):
    # funding
    mocker.patch.object(EthereumClient, "get_balance", return_value=1)

    # bonding
    staking_provider = accounts.staking_providers_accounts[0]
    mock_taco_application_agent.get_staking_provider_from_operator.return_value = (
        staking_provider
    )
    mock_taco_child_application_agent.staking_provider_from_operator.return_value = (
        staking_provider
    )


@pytest.fixture(scope="module")
def mock_accounts():
    accounts = dict()
    for i in range(NUMBER_OF_MOCK_KEYSTORE_ACCOUNTS):
        account = Account.create()
        filename = KEYFILE_NAME_TEMPLATE.format(month=i + 1, address=account.address)
        accounts[filename] = account
    return accounts


@pytest.fixture(scope='module')
def mock_account(mock_accounts):
    return list(mock_accounts.items())[0][1]


@pytest.fixture(scope='module')
def operator_account(mock_accounts, testerchain):
    account = list(mock_accounts.values())[0]
    return account


@pytest.fixture(scope='module')
def operator_address(operator_account):
    address = operator_account.address
    return address


@pytest.fixture(scope='module')
def custom_config_filepath(custom_filepath: Path):
    filepath = custom_filepath / UrsulaConfiguration.generate_filename()
    return filepath


@pytest.fixture(scope='function')
def patch_keystore(mock_accounts, monkeypatch, mocker):
    def successful_mock_keyfile_reader(_keystore, path):

        # Ensure the absolute path is passed to the keyfile reader
        assert MOCK_KEYSTORE_PATH in path
        full_path = path
        del path

        for filename, account in mock_accounts.items():  # Walk the mock filesystem
            if filename in full_path:
                break
        else:
            raise FileNotFoundError(f"No such file {full_path}")
        return account.address, dict(version=3, address=account.address)

    mocker.patch('pathlib.Path.iterdir', return_value=[Path(key) for key in mock_accounts.keys()])
    monkeypatch.setattr(KeystoreSigner, '_KeystoreSigner__read_keystore', successful_mock_keyfile_reader)
    yield
    monkeypatch.delattr(KeystoreSigner, '_KeystoreSigner__read_keystore')


@pytest.fixture(scope='function')
def mock_keystore(mocker):
    mocker.patch.object(KeystoreSigner, '_KeystoreSigner__read_keystore')


@pytest.fixture(scope="module", autouse=True)
def mock_substantiate_stamp(module_mocker, monkeymodule):
    fake_signature = b'\xb1W5?\x9b\xbaix>\'\xfe`\x1b\x9f\xeb*9l\xc0\xa7\xb9V\x9a\x83\x84\x04\x97\x0c\xad\x99\x86\x81W\x93l\xc3\xbde\x03\xcd"Y\xce\xcb\xf7\x02z\xf6\x9c\xac\x84\x05R\x9a\x9f\x97\xf7\xa02\xb2\xda\xa1Gv\x01'
    module_mocker.patch.object(Ursula, "_substantiate_stamp", autospec=True)
    module_mocker.patch.object(Ursula, "operator_signature", fake_signature)
    module_mocker.patch.object(Teacher, "validate_operator")


@pytest.fixture(scope="module", autouse=True)
def mock_transacting_power(module_mocker, monkeymodule):
    module_mocker.patch.object(TransactingPower, "unlock")


@pytest.fixture(scope="module")
def real_operator_get_staking_provider_address():
    _real_get_staking_provider_address = Operator.get_staking_provider_address
    return _real_get_staking_provider_address


@pytest.mark.usefixtures("monkeymodule")
@pytest.fixture(scope="module", autouse=True)
def staking_providers(real_operator_get_staking_provider_address, accounts):
    def faked(self, *args, **kwargs):
        return accounts.staking_providers_accounts[
            accounts.ursulas_accounts.index(self.transacting_power.account)
        ]

    Operator.get_staking_provider_address = faked
    return accounts.staking_providers_accounts


@pytest.fixture(scope="module")
def monkeypatch_get_staking_provider_from_operator(monkeymodule):
    monkeymodule.setattr(
        Operator,
        "get_staking_provider_address",
        lambda self: self.transacting_power.account,
    )



@pytest.fixture(scope="module")
def multichain_ids(module_mocker):
    ids = mock_permitted_multichain_connections(mocker=module_mocker)
    return ids


@pytest.fixture(scope="module")
def multichain_ursulas(ursulas, multichain_ids):
    setup_multichain_ursulas(ursulas=ursulas, chain_ids=multichain_ids)
    return ursulas


@pytest.fixture(scope="module", autouse=True)
def mock_rpc_endpoints(module_mocker):
    """Mock RPC endpoints for integration tests"""

    def mock_get_default_endpoints(domain):
        # Return test endpoints for the testerchain
        return {TESTERCHAIN_CHAIN_ID: ["http://localhost:8545"]}

    module_mocker.patch(
        "nucypher.blockchain.eth.utils.get_default_rpc_endpoints",
        side_effect=mock_get_default_endpoints,
    )


@pytest.fixture(scope="module")
def mock_prometheus(module_mocker):
    return module_mocker.patch("nucypher.characters.lawful.start_prometheus_exporter")


@pytest.fixture(scope="module")
def mock_payment_method(module_mocker):
    module_mocker.patch.object(SubscriptionManagerPayment, "verify", return_value=True)
