from modules.contact_angle.extract_profile import (
    extract_drop_profile,
    detect_edges,
    prepare_hydrophobic,
)
from unittest.mock import patch
from utils.enums import ThresholdSelect

from unittest.mock import patch
import matplotlib.pyplot as plt
import pytest
import numpy as np
import cv2
import os
import sys
import matplotlib

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")))
matplotlib.use("Agg")
plt.show = lambda: None


# Fixtures ----------------------------------------------------------------


@pytest.fixture
def mock_raw_experiment():
    class RawExperiment:
        cropped_image = np.random.randint(0, 255, (100, 100), dtype=np.uint8)
        image = np.random.randint(0, 255, (100, 100), dtype=np.uint8)
        ret = None
        contour = None

    return RawExperiment()


@pytest.fixture
def mock_user_inputs():
    class UserInputs:
        threshold_method = ThresholdSelect.USER_SELECTED
        drop_region = [(10, 10), (90, 90)]
        threshold_val = 127
        original_boole = 0
        cropped_boole = 0
        threshold_boole = 0
        show_popup = 0

    return UserInputs()


@pytest.fixture
def mock_contour_data():
    return np.array(
        [
            [50, -150],
            [55, -145],
            [60, -140],
            [65, -135],
            [70, -130],
            [75, -125],
            [80, -120],
            [85, -115],
            [90, -110],
            [95, -105],
        ],
        dtype=np.float32,
    )


# Core Function Tests -----------------------------------------------------


@patch("cv2.threshold")
@patch("cv2.GaussianBlur")
@patch("cv2.findContours")
def test_manual_threshold_processing(
    mock_findContours, mock_blur, mock_threshold, mock_raw_experiment, mock_user_inputs
):
    mock_threshold.return_value = (
        127,
        np.zeros_like(mock_raw_experiment.cropped_image),
    )
    dummy_contour = np.array([[[50, 50]], [[51, 51]], [[52, 52]]], dtype=np.int32)
    mock_findContours.return_value = ([dummy_contour], None)

    extract_drop_profile(mock_raw_experiment, mock_user_inputs)

    mock_threshold.assert_called_once()
    assert mock_raw_experiment.ret == 127
    assert mock_raw_experiment.contour is not None


@patch("cv2.findContours")
@patch("cv2.threshold")
def test_otsu_auto_threshold(
    mock_threshold, mock_findContours, mock_raw_experiment, mock_user_inputs
):
    mock_user_inputs.threshold_method = ThresholdSelect.AUTOMATED
    mock_threshold.return_value = (
        127,
        np.zeros_like(mock_raw_experiment.cropped_image),
    )
    dummy_contour = np.array([[[50, 50]], [[51, 51]], [[52, 52]]], dtype=np.int32)
    mock_findContours.return_value = ([dummy_contour], None)

    extract_drop_profile(mock_raw_experiment, mock_user_inputs)

    assert 0 <= mock_raw_experiment.ret <= 255
    assert mock_threshold.call_args[0][1] == 0


@patch("cv2.findContours")
def test_contour_trimming(mock_findContours):
    test_contour = np.array([[[0, 0]], [[99, 99]], [[50, 50]]], dtype=np.int32)
    mock_findContours.return_value = ([test_contour], None)

    class DummyRawExp:
        image = np.ones((100, 100), dtype=np.uint8)

    contour, _ = detect_edges(
        np.zeros((100, 100), dtype=np.uint8), DummyRawExp(), None, 1, 127
    )

    assert not any(
        (contour[:, 0] == 0)
        | (contour[:, 0] == 99)
        | (contour[:, 1] == 0)
        | (contour[:, 1] == 99)
    )


# Clustering & Contact Point Tests ----------------------------------------


@pytest.fixture
def mock_contour_data():
    return np.array(
        [
            [40, 160],
            [45, 155],
            [50, 150],
            [55, 145],
            [60, 140],
            [65, 135],
            [70, 130],
            [75, 125],
            [80, 120],
            [85, 115],
            [90, 110],
            [95, 105],
            [100, 100],
        ],
        dtype=np.float32,
    )


@patch("cv2.findContours")
def test_invalid_threshold_handling(mock_findContours, mock_raw_experiment):
    dummy_contour = np.array([[[10, 10]], [[20, 20]], [[30, 30]]], dtype=np.int32)
    mock_findContours.return_value = ([dummy_contour], None)

    contour, _ = detect_edges(
        np.zeros((100, 100), dtype=np.uint8), mock_raw_experiment, None, 1, 300
    )
    assert contour is not None and len(contour) > 0


def test_empty_input_handling():
    with pytest.raises(IndexError):
        prepare_hydrophobic(np.empty((0, 2)))


# Edge Case Tests ---------------------------------------------------------


@patch("cv2.findContours")
def test_low_contrast_image_processing(mock_findContours):
    low_contrast_img = np.full((100, 100), 127, dtype=np.uint8)

    class DummyRawExp:
        image = low_contrast_img

    dummy_contour = np.array([[[50, 50]], [[51, 51]], [[52, 52]]], dtype=np.int32)
    mock_findContours.return_value = ([dummy_contour], None)

    contour, _ = detect_edges(low_contrast_img, DummyRawExp(), None, 1, 127)
    assert len(contour) > 0


@patch("cv2.findContours")
def test_high_noise_handling(mock_findContours):
    mock_findContours.return_value = (
        [np.random.randint(0, 100, (50, 1, 2), dtype=np.int32) for _ in range(10)],
        None,
    )

    class DummyRawExp:
        image = np.zeros((100, 100), dtype=np.uint8)

    contour, _ = detect_edges(
        np.zeros((100, 100), dtype=np.uint8), DummyRawExp(), None, 1, 127
    )
    assert len(contour) > 0


# Main Execution ----------------------------------------------------------


if __name__ == "__main__":
    pytest.main()
