"""Tests for Black-Scholes and Black option pricing functions."""
import pytest
import numpy as np
from qmoms.qmoms import BlackScholes, Black, BlackScholes_delta, Black_delta


class TestBlackScholes:

    def test_atm_call_put_parity(self):
        """Put-call parity: C - P = S - K*exp(-rT)."""
        S, K, r, sigma, T = 100, 100, 0.05, 0.2, 1.0
        c, p = BlackScholes(S, K, r, sigma, T)
        parity = c - p - (S - K * np.exp(-r * T))
        assert abs(parity) < 1e-10

    def test_known_values(self):
        c, p = BlackScholes(100, 100, 0.05, 0.2, 1.0)
        assert abs(c - 10.4505835722) < 1e-6
        assert abs(p - 5.5735260223) < 1e-6

    def test_deep_itm_call(self):
        c, p = BlackScholes(100, 50, 0.05, 0.2, 1.0)
        assert c > 49  # deep ITM call close to intrinsic
        assert p < 0.01  # deep OTM put near zero

    def test_deep_otm_call(self):
        c, p = BlackScholes(100, 200, 0.05, 0.2, 1.0)
        assert c < 0.01  # deep OTM call near zero
        assert p > 89  # deep ITM put close to intrinsic

    def test_zero_vol(self):
        """With zero vol, call = max(S - K*exp(-rT), 0)."""
        S, K, r, T = 100, 90, 0.05, 1.0
        c, p = BlackScholes(S, K, r, 1e-10, T)
        assert abs(c - (S - K * np.exp(-r * T))) < 0.01

    def test_vectorized(self):
        S = np.array([100, 100])
        K = np.array([90, 110])
        c, p = BlackScholes(S, K, 0.05, 0.2, 1.0)
        assert c.shape == (2,)
        assert p.shape == (2,)
        assert c[0] > c[1]  # ITM call > OTM call

    def test_prices_non_negative(self):
        c, p = BlackScholes(100, 100, 0.05, 0.2, 1.0)
        assert c >= 0
        assert p >= 0


class TestBlack:

    def test_atm_call_equals_put(self):
        """For Black model with F=K, call = put."""
        c, p = Black(100, 100, 0.05, 0.2, 1.0)
        assert abs(c - p) < 1e-10

    def test_known_values(self):
        c, p = Black(100, 100, 0.05, 0.2, 1.0)
        assert abs(c - 7.5770821464) < 1e-6
        assert abs(p - 7.5770821464) < 1e-6

    def test_put_call_parity(self):
        """Black put-call parity: C - P = (F - K)*exp(-rT)."""
        F, K, r, sigma, T = 105, 100, 0.05, 0.3, 0.5
        c, p = Black(F, K, r, sigma, T)
        parity = c - p - (F - K) * np.exp(-r * T)
        assert abs(parity) < 1e-10

    def test_prices_non_negative(self):
        c, p = Black(100, 100, 0.05, 0.2, 1.0)
        assert c >= 0
        assert p >= 0


class TestBlackScholesDelta:

    def test_known_values(self):
        dc, dp = BlackScholes_delta(100, 100, 0.05, 0.2, 1.0)
        assert abs(dc - 0.6368306512) < 1e-6
        assert abs(dp - (-0.3631693488)) < 1e-6

    def test_call_put_delta_relation(self):
        """delta_put = delta_call - 1."""
        dc, dp = BlackScholes_delta(100, 100, 0.05, 0.2, 1.0)
        assert abs(dp - (dc - 1)) < 1e-10

    def test_atm_call_delta_around_half(self):
        dc, dp = BlackScholes_delta(100, 100, 0.05, 0.2, 1.0)
        assert 0.5 < dc < 0.8
        assert -0.5 < dp < 0

    def test_deep_itm_call_delta_near_one(self):
        dc, dp = BlackScholes_delta(100, 50, 0.05, 0.2, 1.0)
        assert dc > 0.99

    def test_vectorized(self):
        K = np.array([80, 100, 120])
        dc, dp = BlackScholes_delta(100, K, 0.05, 0.2, 1.0)
        assert dc.shape == (3,)
        assert dc[0] > dc[1] > dc[2]  # delta decreases with K


class TestBlackDelta:

    def test_known_values(self):
        dc, dp = Black_delta(100, 100, 0.05, 0.2, 1.0)
        assert abs(dc - 0.5135001230) < 1e-6
        assert abs(dp - (-0.4377293015)) < 1e-6

    def test_call_delta_bounded(self):
        dc, dp = Black_delta(100, 100, 0.05, 0.2, 1.0)
        assert 0 < dc < 1
        assert -1 < dp < 0
