from unittest.mock import patch
from datetime import datetime
from django.urls import reverse

from api.management.commands.clean_orders import Command as CleanOrders
from api.management.commands.follow_invoices import Command as FollowInvoices
from api.models import Order
from api.tasks import follow_send_payment, send_notification
from tests.utils.node import (
    add_invoice,
    create_address,
    generate_blocks,
    pay_invoice,
    wait_nodes_sync,
)
from tests.utils.pgp import sign_message

maker_form_buy_with_range = {
    "type": Order.Types.BUY,
    "currency": 1,
    "has_range": True,
    "min_amount": 21,
    "max_amount": 101.7,
    "payment_method": "Advcash Cash F2F",
    "is_explicit": False,
    "premium": 3.34,
    "public_duration": 69360,
    "escrow_duration": 8700,
    "bond_size": 3.5,
    "latitude": 34.7455,
    "longitude": 135.503,
}


def read_file(file_path):
    """
    Read a file and return its content.
    """
    with open(file_path, "r") as file:
        return file.read()


class Trade:
    response = None  # Stores the latest response of Order endpoint

    def __init__(
        self,
        client,
        maker_form=maker_form_buy_with_range,
        take_amount=80,
        maker_index=1,
        taker_index=2,
    ):
        self.client = client
        self.maker_form = maker_form
        self.take_amount = take_amount
        self.maker_index = maker_index
        self.taker_index = taker_index

        self.make_order(self.maker_form, maker_index)

    def get_robot_auth(self, robot_index, first_encounter=False):
        """
        Create an AUTH header that embeds token, pub_key, and enc_priv_key into a single string
        as requested by the robosats token middleware.
        """

        b91_token = read_file(f"tests/robots/{robot_index}/b91_token")
        pub_key = read_file(f"tests/robots/{robot_index}/pub_key")
        enc_priv_key = read_file(f"tests/robots/{robot_index}/enc_priv_key")

        # First time a robot authenticated, it is registered by the backend, so pub_key and enc_priv_key is needed
        if first_encounter:
            headers = {
                "HTTP_AUTHORIZATION": f"Token {b91_token} | Public {pub_key} | Private {enc_priv_key}"
            }
        else:
            headers = {"HTTP_AUTHORIZATION": f"Token {b91_token}"}

        return headers

    def create_robot(self, robot_index):
        """
        Creates the robots in /tests/robots/{robot_index}
        """
        path = reverse("robot")
        headers = self.get_robot_auth(robot_index, True)

        return self.client.get(path, **headers)

    def make_order(self, maker_form, robot_index=1):
        """
        Create an order for the test.
        """
        path = reverse("make")
        # Get valid robot auth headers
        headers = self.get_robot_auth(robot_index, True)

        response = self.client.post(path, maker_form, **headers)

        self.response = response
        if response.status_code == 201:
            self.order_id = response.json()["id"]

    def get_order(self, robot_index=1, first_encounter=False):
        """
        Fetch the latest state of the order
        """
        path = reverse("order")
        params = f"?order_id={self.order_id}"
        headers = self.get_robot_auth(robot_index, first_encounter)
        self.response = self.client.get(path + params, **headers)

    @patch("api.tasks.send_notification.delay", send_notification)
    def cancel_order(self, robot_index=1):
        path = reverse("order")
        params = f"?order_id={self.order_id}"
        headers = self.get_robot_auth(robot_index)
        body = {"action": "cancel"}
        self.response = self.client.post(path + params, body, **headers)

    @patch("api.tasks.send_notification.delay", send_notification)
    def send_chat_message(self, message, robot_index=1):
        path = reverse("chat")
        headers = self.get_robot_auth(robot_index)
        body = {"PGP_message": message, "order_id": self.order_id, "offset": 0}
        self.response = self.client.post(path, data=body, **headers)

    @patch("api.tasks.send_notification.delay", send_notification)
    def pause_order(self, robot_index=1):
        path = reverse("order")
        params = f"?order_id={self.order_id}"
        headers = self.get_robot_auth(robot_index)
        body = {"action": "pause"}
        self.response = self.client.post(path + params, body, **headers)

    @patch("api.tasks.send_notification.delay", send_notification)
    def follow_hold_invoices(self):
        # A background thread checks every 5 second the status of invoices. We invoke directly during test.
        follower = FollowInvoices()
        follower.follow_hold_invoices()

    @patch("api.tasks.send_notification.delay", send_notification)
    def clean_orders(self):
        # A background thread checks every 5 second order expirations. We invoke directly during test.
        cleaner = CleanOrders()
        cleaner.clean_orders()

    @patch("api.tasks.follow_send_payment.delay", follow_send_payment)
    def process_payouts(self, mine_a_block=False):
        # A background thread checks every 5 second whether there are outgoing payments. We invoke directly during test.
        follow_invoices = FollowInvoices()
        follow_invoices.send_payments()
        if mine_a_block:
            generate_blocks(create_address("robot"), 1)
            wait_nodes_sync()

    @patch("api.tasks.send_notification.delay", send_notification)
    def publish_order(self):
        # Maker's first order fetch. Should trigger maker bond hold invoice generation.
        self.get_order()
        invoice = self.response.json()["bond_invoice"]

        # Lock the invoice from the robot's node
        pay_invoice("robot", invoice)

        # Check for invoice locked (the mocked LND will return ACCEPTED)
        self.follow_hold_invoices()

        # Get order
        self.get_order()

    @patch("api.tasks.send_notification.delay", send_notification)
    def take_order(self):
        path = reverse("order")
        params = f"?order_id={self.order_id}"
        headers = self.get_robot_auth(self.taker_index, first_encounter=True)
        body = {"action": "take", "amount": self.take_amount}
        self.response = self.client.post(path + params, body, **headers)

    @patch("api.tasks.send_notification.delay", send_notification)
    def lock_taker_bond(self):
        # Takers's first order fetch. Should trigger maker bond hold invoice generation.
        self.get_order(self.taker_index)
        invoice = self.response.json()["bond_invoice"]

        # Lock the invoice from the robot's node
        pay_invoice("robot", invoice)

        # Check for invoice locked (the mocked LND will return ACCEPTED)
        self.follow_hold_invoices()

        # Get order
        self.get_order(self.taker_index)

    @patch("api.tasks.send_notification.delay", send_notification)
    def lock_escrow(self, robot_index):
        # Takers's order fetch. Should trigger trade escrow bond hold invoice generation.
        self.get_order(robot_index)
        invoice = self.response.json()["escrow_invoice"]

        # Lock the invoice from the robot's node
        pay_invoice("robot", invoice)

        # Check for invoice locked (the mocked LND will return ACCEPTED)
        self.follow_hold_invoices()

        # Get order
        self.get_order()

    @patch("api.tasks.send_notification.delay", send_notification)
    def submit_payout_address(self, robot_index=1):
        path = reverse("order")
        params = f"?order_id={self.order_id}"
        headers = self.get_robot_auth(robot_index)

        payout_address = create_address("robot")
        signed_payout_address = sign_message(
            payout_address,
            passphrase_path=f"tests/robots/{robot_index}/token",
            private_key_path=f"tests/robots/{robot_index}/enc_priv_key",
        )
        body = {
            "action": "update_address",
            "address": signed_payout_address,
            "mining_fee_rate": 50,
        }
        self.response = self.client.post(path + params, body, **headers)

    @patch("api.tasks.send_notification.delay", send_notification)
    def submit_payout_invoice(self, robot_index=1, routing_budget=0):
        path = reverse("order")
        params = f"?order_id={self.order_id}"
        headers = self.get_robot_auth(robot_index)

        self.get_order(robot_index)

        payout_invoice = add_invoice("robot", self.response.json()["trade_satoshis"])
        signed_payout_invoice = sign_message(
            payout_invoice,
            passphrase_path=f"tests/robots/{robot_index}/token",
            private_key_path=f"tests/robots/{robot_index}/enc_priv_key",
        )
        body = {
            "action": "update_invoice",
            "invoice": signed_payout_invoice,
            "routing_budget_ppm": routing_budget,
        }

        self.response = self.client.post(path + params, body, **headers)

    @patch("api.tasks.send_notification.delay", send_notification)
    def confirm_fiat(self, robot_index=1):
        path = reverse("order")
        params = f"?order_id={self.order_id}"
        headers = self.get_robot_auth(robot_index)
        body = {"action": "confirm"}
        self.response = self.client.post(path + params, body, **headers)

    @patch("api.tasks.send_notification.delay", send_notification)
    def undo_confirm_sent(self, robot_index=1):
        path = reverse("order")
        params = f"?order_id={self.order_id}"
        headers = self.get_robot_auth(robot_index)
        body = {"action": "undo_confirm"}
        self.response = self.client.post(path + params, body, **headers)

    @patch("api.tasks.send_notification.delay", send_notification)
    def expire_order(self):
        # Change order expiry to now
        order = Order.objects.get(id=self.order_id)
        order.expires_at = datetime.now()
        order.save()

    @patch("api.tasks.send_notification.delay", send_notification)
    def change_order_status(self, status):
        # Change order expiry to now
        order = Order.objects.get(id=self.order_id)
        order.update_status(status)