import asyncio
from pathlib import Path
import sqlite3
import random

import aiosqlite
import pytest
from src.full_node.block_store import BlockStore
from src.full_node.coin_store import CoinStore
from src.full_node.blockchain import Blockchain
from src.types.full_block import FullBlock
from tests.setup_nodes import test_constants, bt


@pytest.fixture(scope="module")
def event_loop():
    loop = asyncio.get_event_loop()
    yield loop


class TestBlockStore:
    @pytest.mark.asyncio
    async def test_block_store(self):
        assert sqlite3.threadsafety == 1
        blocks = bt.get_consecutive_blocks(test_constants, 9, [], 9, b"0")
        blocks_alt = bt.get_consecutive_blocks(test_constants, 3, [], 9, b"1")
        db_filename = Path("blockchain_test.db")
        db_filename_2 = Path("blockchain_test_2.db")
        db_filename_3 = Path("blockchain_test_3.db")

        if db_filename.exists():
            db_filename.unlink()
        if db_filename_2.exists():
            db_filename_2.unlink()
        if db_filename_3.exists():
            db_filename_3.unlink()

        connection = await aiosqlite.connect(db_filename)
        connection_2 = await aiosqlite.connect(db_filename_2)
        connection_3 = await aiosqlite.connect(db_filename_3)

        db = await BlockStore.create(connection)
        # db_2 = await BlockStore.create(connection_2)
        await BlockStore.create(connection_2)
        db_3 = await BlockStore.create(connection_3)
        try:
            genesis = FullBlock.from_bytes(test_constants.GENESIS_BLOCK)

            # Save/get block
            for block in blocks:
                await db.add_block(block)
                assert block == await db.get_block(block.header_hash)

            await db.add_block(blocks_alt[2])
            assert len(await db.get_blocks_at([1, 2])) == 3

            # Get headers (added alt block also, so +1)
            assert len(await db.get_headers()) == len(blocks) + 1

            # Test LCA
            assert (await db.get_lca()) is None
            await db.set_lca(blocks[-3].header_hash)
            assert (await db.get_lca()) == blocks[-3].header
            await db.set_tips([blocks[-2].header_hash, blocks[-1].header_hash])
            assert (await db.get_tips()) == [blocks[-2].header, blocks[-1].header]

            coin_store: CoinStore = await CoinStore.create(connection_3)
            b: Blockchain = await Blockchain.create(coin_store, db_3, test_constants)

            assert b.lca_block == genesis.header
            assert b.tips == [genesis.header]
            assert await db_3.get_lca() == genesis.header
            assert await db_3.get_tips() == [genesis.header]

            for block in blocks:
                await b.receive_block(block)

            assert b.lca_block == blocks[-3].header
            assert set(b.tips) == set(
                [blocks[-3].header, blocks[-2].header, blocks[-1].header]
            )
            left = sorted(b.tips, key=lambda t: t.height)
            right = sorted((await db_3.get_tips()), key=lambda t: t.height)
            assert left == right

        except Exception:
            await connection.close()
            await connection_2.close()
            await connection_3.close()
            db_filename.unlink()
            db_filename_2.unlink()
            db_filename_3.unlink()
            b.shut_down()
            raise

        await connection.close()
        await connection_2.close()
        await connection_3.close()
        db_filename.unlink()
        db_filename_2.unlink()
        db_filename_3.unlink()
        b.shut_down()

    @pytest.mark.asyncio
    async def test_deadlock(self):
        blocks = bt.get_consecutive_blocks(test_constants, 10, [], 9, b"0")
        db_filename = Path("blockchain_test.db")

        if db_filename.exists():
            db_filename.unlink()

        connection = await aiosqlite.connect(db_filename)
        db = await BlockStore.create(connection)
        tasks = []

        for i in range(10000):
            rand_i = random.randint(0, 10)
            if random.random() < 0.5:
                tasks.append(asyncio.create_task(db.add_block(blocks[rand_i])))
            if random.random() < 0.5:
                tasks.append(
                    asyncio.create_task(db.get_block(blocks[rand_i].header_hash))
                )
        await asyncio.gather(*tasks)
        await connection.close()
        db_filename.unlink()
