import numpy as np
import pytest


def test_mb02md_html_doc_example():
    """
    Validate MB02MD with SLICOT HTML doc example.

    Solve TLS problem: AX = B where A is 6x3, B is 6x1.
    JOB='B' computes both RANK and TOL.

    From MB02MD.html:
      M=6, N=3, L=1, JOB='B', sdev=0.0
      Expected: RANK=3, X=[0.5003, 0.8003, 0.2995]
      Singular values: [3.2281, 0.8716, 0.3697, 0.0001]
    """
    from slicot import mb02md

    m, n, l = 6, 3, 1

    c = np.array([
        [0.80010, 0.39985, 0.60005, 0.89999],
        [0.29996, 0.69990, 0.39997, 0.82997],
        [0.49994, 0.60003, 0.20012, 0.79011],
        [0.90013, 0.20016, 0.79995, 0.85002],
        [0.39998, 0.80006, 0.49985, 0.99016],
        [0.20002, 0.90007, 0.70009, 1.02994],
    ], order='F', dtype=float)

    tol = 0.0

    c_out, s, x, rank, rcond, iwarn, info = mb02md('B', m, n, l, c, tol)

    assert info == 0
    assert iwarn == 0
    assert rank == 3

    x_expected = np.array([0.5003, 0.8003, 0.2995], dtype=float)
    np.testing.assert_allclose(x.ravel(), x_expected, rtol=1e-3, atol=1e-4)

    s_expected = np.array([3.2281, 0.8716, 0.3697, 0.0001], dtype=float)
    np.testing.assert_allclose(s, s_expected, rtol=1e-3, atol=1e-4)


def test_mb02md_tls_residual_property():
    """
    Validate TLS mathematical property: (A+DA)*X = B+DB minimizes ||[DA|DB]||_F.

    For the TLS solution, the residual in the original problem should be small
    when we apply the perturbations.

    Random seed: 42 (for reproducibility)
    """
    from slicot import mb02md

    np.random.seed(42)

    m, n, l = 10, 3, 2
    a_true = np.random.randn(m, n).astype(float, order='F')
    x_true = np.random.randn(n, l).astype(float, order='F')
    b_true = a_true @ x_true

    noise_level = 0.01
    a = a_true + noise_level * np.random.randn(m, n)
    b = b_true + noise_level * np.random.randn(m, l)

    c = np.hstack([a, b]).astype(float, order='F')
    tol = 0.0

    c_out, s, x, rank, rcond, iwarn, info = mb02md('B', m, n, l, c.copy(), tol)

    assert info == 0
    assert rank >= 1
    assert rank <= min(m, n)

    residual_orig = np.linalg.norm(a @ x - b, 'fro')
    residual_tls = np.linalg.norm(a @ x - b, 'fro') / np.linalg.norm(b, 'fro')
    assert residual_tls < 1.0


def test_mb02md_singular_values_descending():
    """
    Validate singular values are returned in descending order.

    Mathematical property: S(1) >= S(2) >= ... >= S(p) >= 0

    Random seed: 123 (for reproducibility)
    """
    from slicot import mb02md

    np.random.seed(123)

    m, n, l = 8, 4, 2
    c = np.random.randn(m, n + l).astype(float, order='F')
    tol = 0.0

    c_out, s, x, rank, rcond, iwarn, info = mb02md('B', m, n, l, c.copy(), tol)

    assert info == 0

    p = min(m, n + l)
    assert len(s) == p

    for i in range(p - 1):
        assert s[i] >= s[i + 1] - 1e-14

    assert s[-1] >= -1e-14


def test_mb02md_job_r():
    """
    Validate JOB='R' computes RANK only (TOL must be specified by user).

    Random seed: 456 (for reproducibility)
    """
    from slicot import mb02md

    np.random.seed(456)

    m, n, l = 6, 3, 1
    c = np.random.randn(m, n + l).astype(float, order='F')
    tol = 1e-10

    c_out, s, x, rank, rcond, iwarn, info = mb02md('R', m, n, l, c.copy(), tol)

    assert info == 0
    assert rank >= 0
    assert rank <= min(m, n)


def test_mb02md_job_t():
    """
    Validate JOB='T' computes TOL only (RANK must be specified by user).

    Random seed: 789 (for reproducibility)
    """
    from slicot import mb02md

    np.random.seed(789)

    m, n, l = 6, 3, 1
    c = np.random.randn(m, n + l).astype(float, order='F')
    rank_in = 2
    tol = 0.01

    c_out, s, x, rank, rcond, iwarn, info = mb02md('T', m, n, l, c.copy(), tol, rank=rank_in)

    assert info == 0


def test_mb02md_job_n():
    """
    Validate JOB='N' uses user-specified RANK and TOL.

    Random seed: 888 (for reproducibility)
    """
    from slicot import mb02md

    np.random.seed(888)

    m, n, l = 6, 3, 1
    c = np.random.randn(m, n + l).astype(float, order='F')
    rank_in = 2
    tol = 1e-8

    c_out, s, x, rank, rcond, iwarn, info = mb02md('N', m, n, l, c.copy(), tol, rank=rank_in)

    assert info == 0


def test_mb02md_underdetermined():
    """
    Validate MB02MD handles underdetermined system (M < N).

    Random seed: 456 (for reproducibility)
    """
    from slicot import mb02md

    np.random.seed(456)

    m, n, l = 4, 6, 2
    c = np.random.randn(m, n + l).astype(float, order='F')
    tol = 0.0

    c_out, s, x, rank, rcond, iwarn, info = mb02md('B', m, n, l, c.copy(), tol)

    if info > 0:
        pytest.skip(f"SVD did not converge (info={info}) - platform-dependent numerical issue")
    assert info == 0
    assert x.shape == (n, l)


def test_mb02md_multiple_rhs():
    """
    Validate MB02MD with multiple right-hand sides (L > 1).

    Random seed: 1111 (for reproducibility)
    """
    from slicot import mb02md

    np.random.seed(1111)

    m, n, l = 10, 4, 3
    c = np.random.randn(m, n + l).astype(float, order='F')
    tol = 0.0

    c_out, s, x, rank, rcond, iwarn, info = mb02md('B', m, n, l, c.copy(), tol)

    assert info == 0
    assert x.shape == (n, l)


def test_mb02md_edge_case_l_zero():
    """
    Validate edge case: L=0 (no observation matrix B).
    """
    from slicot import mb02md

    np.random.seed(2222)

    m, n, l = 5, 3, 0
    c = np.random.randn(m, n).astype(float, order='F')
    tol = 0.0

    c_out, s, x, rank, rcond, iwarn, info = mb02md('B', m, n, l, c.copy(), tol)

    assert info == 0


def test_mb02md_error_invalid_m():
    """
    Validate error handling: invalid M (negative).
    """
    from slicot import mb02md

    c = np.array([[1.0, 2.0]], order='F', dtype=float)

    with pytest.raises((ValueError, RuntimeError)):
        mb02md('B', -1, 1, 1, c, 0.0)


def test_mb02md_error_invalid_job():
    """
    Validate error handling: invalid JOB parameter.
    """
    from slicot import mb02md

    c = np.array([[1.0, 2.0]], order='F', dtype=float)

    with pytest.raises((ValueError, RuntimeError)):
        mb02md('X', 1, 1, 1, c, 0.0)
