import random
from datetime import datetime, timedelta
from http import HTTPStatus
from math import ceil, floor
from unittest.mock import patch

import pytest

from constants import InvoiceStatus, OrderStatus, ORDER_FETCH_STATES
from database import db
from models import Order, TxRetry
import constants
import server

from common import new_invoice, place_order, generate_test_order
from error import assert_error


def place_orders(client,
                 mock_new_invoice,
                 n_orders,
                 n_bytes,
                 target_state='pending',
                 channel=1,
                 admin=False):
    # target_state refers to one of the order fetching states accepted by the
    # /orders/:state endpoint. Set the target order status based on that.
    if target_state in ['queued', 'retransmitting']:
        order_status = OrderStatus.transmitting.value
    elif target_state == 'rx-pending':
        order_status = OrderStatus.sent.value
    else:
        order_status = OrderStatus[target_state].value

    order_list = []
    for i in range(n_orders):
        # Randomize the bid
        bid_per_byte = random.randint(1, 10000)
        bid = bid_per_byte * n_bytes

        # Randomize the relevant timestamps
        tstamp = datetime.utcnow() - timedelta(
            seconds=random.randint(1, 10000))

        mock_new_invoice.return_value = (True,
                                         new_invoice(1, InvoiceStatus.pending,
                                                     bid))

        post_rv = place_order(client,
                              n_bytes,
                              bid=bid,
                              channel=channel,
                              admin=admin)
        assert post_rv.status_code == HTTPStatus.OK
        uuid = post_rv.get_json()['uuid']

        # Set the status and the fields required in each status
        db_order = Order.query.filter_by(uuid=uuid).first()
        db_order.created_at = tstamp
        db_order.status = order_status
        if target_state in ['transmitting', 'confirming', 'retransmitting']:
            db_order.started_transmission_at = tstamp
        if target_state in ['sent', 'received', 'rx-pending']:
            db_order.ended_transmission_at = tstamp
        if target_state == 'retransmitting':
            new_retry_tx = TxRetry(order_id=db_order.id,
                                   region_code=0,
                                   last_attempt=tstamp)
            db.session.add(new_retry_tx)
        db.session.commit()
        order_list.append(db_order)
    return order_list


@pytest.fixture
def client(mockredis):
    app = server.create_app(from_test=True)
    app.app_context().push()
    with app.test_client() as client:
        yield client
    server.teardown_app(app)


@pytest.mark.parametrize("param", ['before', 'after'])
def test_get_orders_invalid_before_after_parameter(client, param):
    for tstamp in [
            '', '2021-13-11', '2021.05.10', '2021-05-1a0T19:51:45',
            '2021.05.10T19:51:45', '2021-05-10T25:51:45'
    ]:
        get_rv = client.get(f'/orders/pending?{param}={tstamp}')
        assert get_rv.status_code == HTTPStatus.BAD_REQUEST
        assert f'{param}' in get_rv.get_json()

    get_rv = client.get(f'/orders/pending?{param}=2021-05-10T22:51:45')
    assert get_rv.status_code == HTTPStatus.OK
    # before and before_delta together should not be allowed. Same for after
    # and after_delta.
    get_rv = client.get(
        f'/orders/pending?{param}=2021-05-10T22:51:45&{param}_delta=5')
    assert get_rv.status_code == HTTPStatus.BAD_REQUEST


@pytest.mark.parametrize("param", ['before', 'after'])
def test_get_orders_invalid_before_after_delta_parameter(client, param):
    for delta in ['', 'sometext', '2021-05-10T25:51:45', '5.2']:
        get_rv = client.get(f'/orders/pending?{param}_delta={delta}')
        assert get_rv.status_code == HTTPStatus.BAD_REQUEST
        assert f'{param}_delta' in get_rv.get_json()

    get_rv = client.get(f'/orders/pending?{param}_delta=5')
    assert get_rv.status_code == HTTPStatus.OK
    # before and before_delta together should not be allowed. Same for after
    # and after_delta.
    get_rv = client.get(
        f'/orders/pending?{param}_delta=5&{param}=2021-05-10T22:51:45')
    assert get_rv.status_code == HTTPStatus.BAD_REQUEST


def test_get_orders_invalid_limit(client):
    for limit in [
            '',
            'sometext',
            '1a2',
            '-1',
            '1.2',
            constants.MAX_PAGE_SIZE + 1,
    ]:
        get_rv = client.get(f'/orders/pending?limit={limit}')
        assert get_rv.status_code == HTTPStatus.BAD_REQUEST
        assert 'limit' in get_rv.get_json()

    get_rv = client.get('/orders/pending?limit=1')
    assert get_rv.status_code == HTTPStatus.OK
    get_rv = client.get(f'/orders/pending?limit={constants.MAX_PAGE_SIZE}')
    assert get_rv.status_code == HTTPStatus.OK


def test_try_to_get_invalid_order_state(client):
    for state in ['pendiing', 'someendpoint', 'Pending']:
        get_rv = client.get(f'/orders/{state}')
        assert get_rv.status_code == HTTPStatus.NOT_FOUND


@patch('constants.PAGE_SIZE', 3)  # change PAGE_SIZE to run this test faster
@patch('orders.new_invoice')
@pytest.mark.parametrize("state", ORDER_FETCH_STATES)
def test_get_orders_date_filtering_parameters(mock_new_invoice, client, state):
    # Create PAGE_SIZE orders with random timestamps
    n_bytes = 500
    n_orders = constants.PAGE_SIZE
    order_list = place_orders(client, mock_new_invoice, n_orders, n_bytes,
                              state)
    sorted_orders = sorted(order_list,
                           key=lambda x: x.created_at,
                           reverse=True)
    order_uuids = [x.uuid for x in sorted_orders]
    order_creation_timestamps = sorted([x.created_at for x in order_list],
                                       reverse=True)

    # Fetch orders excluding the most recent
    uuid_set1 = set(order_uuids[1:])
    before = order_creation_timestamps[0].isoformat()
    get_rv = client.get(f'/orders/{state}?before={before}')
    assert get_rv.status_code == HTTPStatus.OK
    assert set([x['uuid'] for x in get_rv.get_json()]) == uuid_set1

    # Fetch orders excluding the oldest
    uuid_set2 = set(order_uuids[:-1])
    after = order_creation_timestamps[-1].isoformat()
    get_rv = client.get(f'/orders/{state}?after={after}')
    assert get_rv.status_code == HTTPStatus.OK
    assert set([x['uuid'] for x in get_rv.get_json()]) == uuid_set2

    # Fetch the two orders in the middle
    uuid_set3 = set(order_uuids[1:-1])
    get_rv = client.get(f'/orders/{state}?after={after}&before={before}')
    assert get_rv.status_code == HTTPStatus.OK
    assert set([x['uuid'] for x in get_rv.get_json()]) == uuid_set3

    # Try the same using the delta parameters

    # Fetch orders excluding the most recent
    delta_to_most_recent = datetime.utcnow() - order_creation_timestamps[0]
    before_delta = ceil(delta_to_most_recent.total_seconds()) + 1
    get_rv = client.get(f'/orders/{state}?before_delta={before_delta}')
    assert get_rv.status_code == HTTPStatus.OK
    assert set([x['uuid'] for x in get_rv.get_json()]) == uuid_set1

    # Fetch orders excluding the oldest
    delta_to_oldest = datetime.utcnow() - order_creation_timestamps[-1]
    after_delta = floor(delta_to_oldest.total_seconds()) - 1
    get_rv = client.get(f'/orders/{state}?after_delta={after_delta}')
    assert get_rv.status_code == HTTPStatus.OK
    assert set([x['uuid'] for x in get_rv.get_json()]) == uuid_set2

    # Fetch the two orders in the middle
    get_rv = client.get(f'/orders/{state}?after_delta={after_delta}&' +
                        f'before_delta={before_delta}')
    assert get_rv.status_code == HTTPStatus.OK
    assert set([x['uuid'] for x in get_rv.get_json()]) == uuid_set3


@patch('orders.new_invoice')
@pytest.mark.parametrize("state", ['pending', 'retransmitting'])
def test_get_orders_limit_parameter(mock_new_invoice, client, state):
    n_bytes = 500
    n_orders = constants.PAGE_SIZE + 1
    place_orders(client, mock_new_invoice, n_orders, n_bytes, state)

    # no limit parameter, max PAGE_SIZE should be returned
    get_rv = client.get(f'/orders/{state}')
    assert get_rv.status_code == HTTPStatus.OK
    get_json_resp = get_rv.get_json()
    assert len(get_json_resp) == constants.PAGE_SIZE

    # with limit parameter present
    get_rv = client.get(f'/orders/{state}?limit=10')
    assert get_rv.status_code == HTTPStatus.OK
    get_json_resp = get_rv.get_json()
    assert len(get_json_resp) == 10


@patch('constants.PAGE_SIZE', 3)  # change PAGE_SIZE to run this test faster
@patch('orders.new_invoice')
@pytest.mark.parametrize("state", ORDER_FETCH_STATES)
@pytest.mark.parametrize("channel", constants.CHANNELS)
def test_get_orders_by_channel(mock_new_invoice, client, state, channel):
    n_bytes = 500
    n_orders = constants.PAGE_SIZE + 1
    place_orders(client,
                 mock_new_invoice,
                 n_orders,
                 n_bytes,
                 state,
                 channel=channel,
                 admin=True)

    # Get the orders of each channel as a regular user
    for _channel in constants.CHANNELS:
        get_rv = client.get(f'/orders/{state}?channel={_channel}')
        if 'get' in constants.CHANNEL_INFO[_channel].user_permissions:
            assert get_rv.status_code == HTTPStatus.OK
            get_json_resp = get_rv.get_json()
            n_expected_res = constants.PAGE_SIZE if _channel == channel else 0
            assert len(get_json_resp) == n_expected_res
        else:
            assert get_rv.status_code == HTTPStatus.UNAUTHORIZED
            assert_error(get_rv.get_json(), 'ORDER_CHANNEL_UNAUTHORIZED_OP')

    # Get the orders of each channel as an admin user
    for _channel in constants.CHANNELS:
        get_rv = client.get(f'/admin/orders/{state}?channel={_channel}')
        assert get_rv.status_code == HTTPStatus.OK
        get_json_resp = get_rv.get_json()
        n_expected_res = constants.PAGE_SIZE if _channel == channel else 0
        assert len(get_json_resp) == n_expected_res


@patch('orders.new_invoice')
@patch('constants.PAGE_SIZE', 3)  # change PAGE_SIZE to run this test faster
def test_get_orders_paging(mock_new_invoice, client):
    # make more orders than PAGE_SIZE
    n_orders = constants.PAGE_SIZE + 2
    n_bytes = 500
    order_list = place_orders(client, mock_new_invoice, n_orders, n_bytes)
    sorted_orders = sorted(order_list,
                           key=lambda x: x.created_at,
                           reverse=True)
    order_uuids = [x.uuid for x in sorted_orders]

    # Check all orders were created
    all_orders = Order.query.filter_by(status=OrderStatus.pending.value).all()
    assert len(all_orders) == n_orders

    # Fetch the pending orders with paging
    get_rv = client.get('/orders/pending')
    assert get_rv.status_code == HTTPStatus.OK
    resp_uuids = [x['uuid'] for x in get_rv.get_json()]

    # Only the last PAGE_SIZE orders should be returned
    assert len(resp_uuids) == constants.PAGE_SIZE
    assert resp_uuids == order_uuids[:constants.PAGE_SIZE]


@patch('orders.new_invoice')
@pytest.mark.parametrize("queue", ['queued', 'sent'])
def test_get_orders_multi_state_queues(mock_new_invoice, client, queue):
    """Test queues that return orders in multiple state"""
    # Create some orders with different states
    order_dict = {}
    for state in [
            'pending', 'paid', 'transmitting', 'sent', 'received', 'confirming'
    ]:
        order_dict[state] = generate_test_order(
            mock_new_invoice, client, order_status=OrderStatus[state])
        if state in ['sent', 'received']:
            db_order = Order.query.filter_by(
                uuid=order_dict[state]['uuid']).first()
            db_order.ended_transmission_at = datetime.utcnow()
            db.session.commit()

    # Fetch the orders from the chosen queue
    get_rv = client.get(f'/orders/{queue}')
    assert get_rv.status_code == HTTPStatus.OK
    queued_uuids = [order['uuid'] for order in get_rv.get_json()]

    # Status that can be expected in the chosen queue
    expected_status = {
        'queued': ['paid', 'transmitting', 'confirming'],
        'sent': ['sent', 'received']
    }

    for state in order_dict:
        if state in expected_status[queue]:
            assert order_dict[state]['uuid'] in queued_uuids
        else:
            assert order_dict[state]['uuid'] not in queued_uuids


@patch('orders.new_invoice')
@pytest.mark.parametrize("state", ORDER_FETCH_STATES)
def test_get_orders_sorting(mock_new_invoice, client, state):
    n_bytes = 500
    n_orders = 10
    order_list = place_orders(client, mock_new_invoice, n_orders, n_bytes,
                              state)

    if state in ['pending', 'paid']:
        # sorted by created_at
        expected_sorted_orders = sorted(order_list,
                                        key=lambda x: x.created_at,
                                        reverse=True)
    elif state in ['transmitting', 'confirming', 'retransmitting']:
        # sorted by started_transmission_at
        expected_sorted_orders = sorted(
            order_list, key=lambda x: x.started_transmission_at, reverse=True)
    elif state in ['sent', 'received', 'rx-pending']:
        # sorted by ended_transmission_at
        expected_sorted_orders = sorted(order_list,
                                        key=lambda x: x.ended_transmission_at,
                                        reverse=True)
    elif state == 'queued':
        # sorted by bid_per_byte
        expected_sorted_orders = sorted(order_list,
                                        key=lambda x: x.bid_per_byte,
                                        reverse=True)

    get_rv = client.get(f'/orders/{state}')
    assert get_rv.status_code == HTTPStatus.OK
    get_json_resp = get_rv.get_json()
    assert len(get_json_resp) == n_orders
    for i in range(n_orders):
        assert get_json_resp[i]['uuid'] == expected_sorted_orders[i].uuid
        assert get_json_resp[i]['bid_per_byte'] == expected_sorted_orders[
            i].bid_per_byte


@patch('orders.new_invoice')
@patch('constants.FORCE_PAYMENT', True)
def test_create_order_with_force_payment_enabled(mock_new_invoice, client):
    uuid_order1 = generate_test_order(mock_new_invoice, client)['uuid']
    uuid_order2 = generate_test_order(mock_new_invoice, client)['uuid']

    db_order1 = Order.query.filter_by(uuid=uuid_order1).first()
    db_invoice1 = db_order1.invoices[0]

    db_order2 = Order.query.filter_by(uuid=uuid_order2).first()
    db_invoice2 = db_order2.invoices[0]

    # Since FORCE_PAYMENT is set and both orders have only one invoice, both
    # orders change their statuses to paid. Furthermore, the payment triggers a
    # Tx start verification and, since the transmit queue is empty, order1
    # immediately changes to transmitting state. In contrast, order2 stays in
    # paid state as it needs to wait until the transmission of order1 finishes.
    assert db_order1.status == OrderStatus.transmitting.value
    assert db_invoice1.status == InvoiceStatus.paid.value
    assert db_invoice1.paid_at is not None

    assert db_order2.status == OrderStatus.paid.value
    assert db_invoice2.status == InvoiceStatus.paid.value
    assert db_invoice2.paid_at is not None