"""Extra coverage tests for notifications — service, manager, and url_builder gaps."""

from collections import deque
from datetime import datetime, timedelta, UTC
from unittest.mock import MagicMock

import pytest

from local_deep_research.notifications.manager import (
    NotificationManager,
    RateLimiter,
)
from local_deep_research.notifications.templates import EventType
from local_deep_research.security.url_builder import (
    build_base_url_from_settings,
    build_full_url,
    mask_sensitive_url,
    normalize_bind_address,
    validate_constructed_url,
    URLBuilderError,
)

SVC_MODULE = "local_deep_research.notifications.service"
MGR_MODULE = "local_deep_research.notifications.manager"


# ===========================================================================
# NotificationManager._should_notify exception path
# ===========================================================================


class TestShouldNotifyException:
    def test_exception_returns_false(self):
        """_should_notify returns False on exception to avoid loops."""
        mgr = MagicMock(spec=NotificationManager)
        mgr._get_setting = MagicMock(side_effect=RuntimeError("db fail"))

        # Call the real method
        result = NotificationManager._should_notify(
            mgr, EventType.RESEARCH_COMPLETED
        )
        assert result is False

    def test_enabled_returns_true(self):
        mgr = MagicMock(spec=NotificationManager)
        mgr._get_setting = MagicMock(return_value=True)

        result = NotificationManager._should_notify(
            mgr, EventType.RESEARCH_COMPLETED
        )
        assert result is True

    def test_disabled_returns_false(self):
        mgr = MagicMock(spec=NotificationManager)
        mgr._get_setting = MagicMock(return_value=False)

        result = NotificationManager._should_notify(
            mgr, EventType.RESEARCH_COMPLETED
        )
        assert result is False


# ===========================================================================
# NotificationManager._log_notification exception path
# ===========================================================================


class TestLogNotificationException:
    def test_exception_swallowed(self):
        """_log_notification swallows exceptions gracefully."""
        mgr = MagicMock(spec=NotificationManager)
        mgr._user_id = "testuser"

        # context.get raising should be caught
        bad_context = MagicMock()
        bad_context.get.side_effect = RuntimeError("context error")

        NotificationManager._log_notification(
            mgr, EventType.RESEARCH_COMPLETED, bad_context
        )

    def test_logs_query_from_context(self):
        mgr = MagicMock(spec=NotificationManager)
        mgr._user_id = "testuser"

        NotificationManager._log_notification(
            mgr,
            EventType.RESEARCH_COMPLETED,
            {"query": "test query"},
        )


# ===========================================================================
# RateLimiter._clean_old_entries
# ===========================================================================


class TestRateLimiterCleanOldEntries:
    def test_cleans_hourly_entries(self):
        limiter = RateLimiter(max_per_hour=10, max_per_day=100)
        user = "testuser"

        now = datetime.now(UTC)
        old = now - timedelta(hours=2)

        limiter._hourly_counts[user] = deque([old, old, now])
        limiter._daily_counts[user] = deque([now])

        limiter._clean_old_entries(user, now)

        assert len(limiter._hourly_counts[user]) == 1

    def test_cleans_daily_entries(self):
        limiter = RateLimiter(max_per_hour=10, max_per_day=100)
        user = "testuser"

        now = datetime.now(UTC)
        old = now - timedelta(days=2)

        limiter._hourly_counts[user] = deque([now])
        limiter._daily_counts[user] = deque([old, old, now])

        limiter._clean_old_entries(user, now)

        assert len(limiter._daily_counts[user]) == 1

    def test_cleans_all_old_entries(self):
        limiter = RateLimiter(max_per_hour=10, max_per_day=100)
        user = "testuser"

        now = datetime.now(UTC)
        old_h = now - timedelta(hours=2)
        old_d = now - timedelta(days=2)

        limiter._hourly_counts[user] = deque([old_h])
        limiter._daily_counts[user] = deque([old_d])

        limiter._clean_old_entries(user, now)

        assert len(limiter._hourly_counts[user]) == 0
        assert len(limiter._daily_counts[user]) == 0


# ===========================================================================
# RateLimiter._cleanup_inactive_users_if_needed
# ===========================================================================


class TestRateLimiterCleanupInactiveUsers:
    def test_skips_if_not_due(self):
        limiter = RateLimiter(max_per_hour=10, max_per_day=100)
        now = datetime.now(UTC)
        limiter._last_cleanup = now  # just cleaned up

        limiter._hourly_counts["old_user"] = deque()
        limiter._cleanup_inactive_users_if_needed(now)

        # Should not have cleaned up (too soon)
        assert "old_user" in limiter._hourly_counts

    def test_removes_inactive_users(self):
        limiter = RateLimiter(
            max_per_hour=10, max_per_day=100, cleanup_interval_hours=1
        )
        now = datetime.now(UTC)
        # Force cleanup to be due
        limiter._last_cleanup = now - timedelta(hours=2)

        # Old inactive user
        old_time = now - timedelta(days=8)
        limiter._hourly_counts["inactive"] = deque([old_time])
        limiter._daily_counts["inactive"] = deque([old_time])

        # Recent active user
        limiter._hourly_counts["active"] = deque([now])
        limiter._daily_counts["active"] = deque([now])

        limiter._cleanup_inactive_users_if_needed(now)

        assert "inactive" not in limiter._hourly_counts
        assert "active" in limiter._hourly_counts

    def test_empty_queues_treated_as_inactive(self):
        limiter = RateLimiter(
            max_per_hour=10, max_per_day=100, cleanup_interval_hours=1
        )
        now = datetime.now(UTC)
        limiter._last_cleanup = now - timedelta(hours=2)

        limiter._hourly_counts["empty_user"] = deque()
        limiter._daily_counts["empty_user"] = deque()

        limiter._cleanup_inactive_users_if_needed(now)

        assert "empty_user" not in limiter._hourly_counts


# ===========================================================================
# RateLimiter.reset
# ===========================================================================


class TestRateLimiterReset:
    def test_reset_specific_user(self):
        limiter = RateLimiter(max_per_hour=10, max_per_day=100)
        limiter._hourly_counts["user1"] = deque([datetime.now(UTC)])
        limiter._hourly_counts["user2"] = deque([datetime.now(UTC)])

        limiter.reset("user1")

        assert "user1" not in limiter._hourly_counts
        assert "user2" in limiter._hourly_counts

    def test_reset_all_users(self):
        limiter = RateLimiter(max_per_hour=10, max_per_day=100)
        limiter._hourly_counts["user1"] = deque([datetime.now(UTC)])
        limiter._hourly_counts["user2"] = deque([datetime.now(UTC)])

        limiter.reset()

        assert len(limiter._hourly_counts) == 0
        assert len(limiter._daily_counts) == 0


# ===========================================================================
# NotificationService._validate_url
# ===========================================================================


class TestValidateUrl:
    def test_empty_url_raises(self):
        from local_deep_research.notifications.service import (
            NotificationService,
            ServiceError,
        )

        with pytest.raises(ServiceError, match="non-empty"):
            NotificationService._validate_url("")

    def test_none_url_raises(self):
        from local_deep_research.notifications.service import (
            NotificationService,
            ServiceError,
        )

        with pytest.raises(ServiceError, match="non-empty"):
            NotificationService._validate_url(None)

    def test_no_scheme_raises(self):
        from local_deep_research.notifications.service import (
            NotificationService,
            ServiceError,
        )

        with pytest.raises(ServiceError, match="Invalid URL"):
            NotificationService._validate_url("no-scheme-here")

    def test_valid_url_passes(self):
        from local_deep_research.notifications.service import (
            NotificationService,
        )

        NotificationService._validate_url("discord://webhook/token")


# ===========================================================================
# NotificationService.get_service_type
# ===========================================================================


class TestGetServiceType:
    def test_discord_detected(self):
        from local_deep_research.notifications.service import (
            NotificationService,
        )

        svc = MagicMock(spec=NotificationService)
        svc.SERVICE_PATTERNS = NotificationService.SERVICE_PATTERNS
        result = NotificationService.get_service_type(
            svc, "discord://webhook_id/webhook_token"
        )
        assert result == "discord"

    def test_unknown_url(self):
        from local_deep_research.notifications.service import (
            NotificationService,
        )

        svc = MagicMock(spec=NotificationService)
        svc.SERVICE_PATTERNS = NotificationService.SERVICE_PATTERNS
        result = NotificationService.get_service_type(svc, "custom://something")
        assert result == "unknown"


# ===========================================================================
# url_builder — mask_sensitive_url
# ===========================================================================


class TestMaskSensitiveUrl:
    def test_masks_password(self):
        result = mask_sensitive_url("https://user:secret123@host.com/path")
        assert "secret123" not in result
        assert "***" in result

    def test_masks_long_path_tokens(self):
        result = mask_sensitive_url(
            "https://discord.com/api/webhooks/abcdefghijklmnopqrstuvwxyz123456"
        )
        assert "/***" in result

    def test_masks_query_params(self):
        result = mask_sensitive_url(
            "https://host.com/path?token=secret&key=api"
        )
        assert "?***" in result

    def test_no_password_no_mask(self):
        result = mask_sensitive_url("https://host.com/short")
        assert "***" not in result or result == "https://host.com/short"

    def test_invalid_url_returns_scheme_mask(self):
        result = mask_sensitive_url("badscheme://something")
        assert isinstance(result, str)

    def test_empty_path(self):
        result = mask_sensitive_url("https://host.com")
        assert "host.com" in result


# ===========================================================================
# url_builder — build_base_url_from_settings
# ===========================================================================


class TestBuildBaseUrlFromSettings:
    def test_external_url_priority(self):
        result = build_base_url_from_settings(
            external_url="https://myapp.com",
            host="localhost",
            port=5000,
        )
        assert result == "https://myapp.com"

    def test_external_url_strips_trailing_slash(self):
        result = build_base_url_from_settings(external_url="https://myapp.com/")
        assert result == "https://myapp.com"

    def test_host_port_construction(self):
        result = build_base_url_from_settings(host="myhost", port=8080)
        assert result == "http://myhost:8080"

    def test_bind_address_normalized(self):
        result = build_base_url_from_settings(host="0.0.0.0", port=5000)
        assert result == "http://localhost:5000"

    def test_fallback(self):
        result = build_base_url_from_settings(
            fallback_base="http://custom:9999"
        )
        assert result == "http://custom:9999"

    def test_empty_external_url_ignored(self):
        result = build_base_url_from_settings(
            external_url="  ",
            host="myhost",
            port=3000,
        )
        assert result == "http://myhost:3000"


# ===========================================================================
# url_builder — validate_constructed_url
# ===========================================================================


class TestValidateConstructedUrl:
    def test_valid_url(self):
        assert validate_constructed_url("https://example.com/path") is True

    def test_empty_url_raises(self):
        with pytest.raises(URLBuilderError, match="non-empty"):
            validate_constructed_url("")

    def test_none_raises(self):
        with pytest.raises(URLBuilderError, match="non-empty"):
            validate_constructed_url(None)

    def test_no_scheme_raises(self):
        with pytest.raises(URLBuilderError, match="scheme"):
            validate_constructed_url("example.com/path")

    def test_disallowed_scheme_raises(self):
        with pytest.raises(URLBuilderError, match="not in allowed"):
            validate_constructed_url(
                "ftp://example.com", allowed_schemes=["http", "https"]
            )

    def test_no_hostname_raises(self):
        with pytest.raises(URLBuilderError, match="hostname"):
            validate_constructed_url("http://")


# ===========================================================================
# url_builder — build_full_url
# ===========================================================================


class TestBuildFullUrl:
    def test_basic(self):
        result = build_full_url("https://myapp.com", "/research/123")
        assert result == "https://myapp.com/research/123"

    def test_adds_leading_slash(self):
        result = build_full_url(
            "https://myapp.com", "research/123", validate=False
        )
        assert result == "https://myapp.com/research/123"

    def test_strips_trailing_slash_from_base(self):
        result = build_full_url("https://myapp.com/", "/path", validate=False)
        assert result == "https://myapp.com/path"

    def test_validation_enabled(self):
        result = build_full_url("https://myapp.com", "/path")
        assert result == "https://myapp.com/path"

    def test_validation_fails_raises(self):
        with pytest.raises(URLBuilderError):
            build_full_url("", "/path")


# ===========================================================================
# url_builder — normalize_bind_address
# ===========================================================================


class TestNormalizeBindAddress:
    def test_bind_all_ipv4(self):
        assert normalize_bind_address("0.0.0.0") == "localhost"

    def test_bind_all_ipv6(self):
        assert normalize_bind_address("::") == "localhost"

    def test_normal_host_unchanged(self):
        assert normalize_bind_address("myhost") == "myhost"

    def test_localhost_unchanged(self):
        assert normalize_bind_address("localhost") == "localhost"
