"""
Tests for security/url_builder.py

Tests cover:
- normalize_bind_address() function
- build_base_url_from_settings() function
- build_full_url() function
- validate_constructed_url() function
- mask_sensitive_url() function
"""

import pytest

from local_deep_research.security.url_builder import (
    URLBuilderError,
    normalize_bind_address,
    build_base_url_from_settings,
    build_full_url,
    validate_constructed_url,
    mask_sensitive_url,
)


class TestNormalizeBindAddress:
    """Tests for normalize_bind_address function."""

    def test_ipv4_bind_all_converts_to_localhost(self):
        """Test that 0.0.0.0 is converted to localhost."""
        result = normalize_bind_address("0.0.0.0")
        assert result == "localhost"

    def test_ipv6_bind_all_converts_to_localhost(self):
        """Test that :: is converted to localhost."""
        result = normalize_bind_address("::")
        assert result == "localhost"

    def test_regular_hostname_unchanged(self):
        """Test that regular hostnames are not modified."""
        result = normalize_bind_address("myserver.example.com")
        assert result == "myserver.example.com"

    def test_localhost_unchanged(self):
        """Test that localhost is not modified."""
        result = normalize_bind_address("localhost")
        assert result == "localhost"

    def test_regular_ip_unchanged(self):
        """Test that regular IP addresses are not modified."""
        result = normalize_bind_address("192.168.1.100")
        assert result == "192.168.1.100"

    def test_127_0_0_1_unchanged(self):
        """Test that 127.0.0.1 is not modified (specific localhost IP)."""
        result = normalize_bind_address("127.0.0.1")
        assert result == "127.0.0.1"


class TestBuildBaseUrlFromSettings:
    """Tests for build_base_url_from_settings function."""

    def test_external_url_takes_priority(self):
        """Test that external_url is used when provided."""
        result = build_base_url_from_settings(
            external_url="https://myapp.example.com",
            host="localhost",
            port=5000,
        )
        assert result == "https://myapp.example.com"

    def test_external_url_strips_trailing_slash(self):
        """Test that trailing slashes are removed from external_url."""
        result = build_base_url_from_settings(
            external_url="https://myapp.example.com/"
        )
        assert result == "https://myapp.example.com"

    def test_external_url_strips_whitespace(self):
        """Test that whitespace is stripped from external_url."""
        result = build_base_url_from_settings(
            external_url="  https://myapp.example.com  "
        )
        assert result == "https://myapp.example.com"

    def test_host_port_used_when_no_external_url(self):
        """Test that host and port are used when external_url is None."""
        result = build_base_url_from_settings(
            external_url=None,
            host="myserver",
            port=8080,
        )
        assert result == "http://myserver:8080"

    def test_host_port_normalizes_bind_address(self):
        """Test that bind addresses are normalized when building URL."""
        result = build_base_url_from_settings(
            external_url=None,
            host="0.0.0.0",
            port=5000,
        )
        assert result == "http://localhost:5000"

    def test_port_can_be_string(self):
        """Test that port can be provided as string."""
        result = build_base_url_from_settings(
            external_url=None,
            host="localhost",
            port="3000",
        )
        assert result == "http://localhost:3000"

    def test_fallback_used_when_no_host_port(self):
        """Test that fallback is used when no external_url or host/port."""
        result = build_base_url_from_settings(
            external_url=None,
            host=None,
            port=None,
            fallback_base="http://fallback.local:9000",
        )
        assert result == "http://fallback.local:9000"

    def test_default_fallback(self):
        """Test that default fallback is http://localhost:5000."""
        result = build_base_url_from_settings()
        assert result == "http://localhost:5000"

    def test_fallback_strips_trailing_slash(self):
        """Test that trailing slashes are removed from fallback."""
        result = build_base_url_from_settings(
            fallback_base="http://localhost:5000/"
        )
        assert result == "http://localhost:5000"

    def test_empty_external_url_uses_fallback(self):
        """Test that empty string external_url triggers fallback."""
        result = build_base_url_from_settings(
            external_url="",
            host=None,
            port=None,
        )
        assert result == "http://localhost:5000"

    def test_whitespace_only_external_url_uses_fallback(self):
        """Test that whitespace-only external_url triggers fallback."""
        result = build_base_url_from_settings(
            external_url="   ",
            host=None,
            port=None,
        )
        assert result == "http://localhost:5000"

    def test_invalid_port_raises_error(self):
        """Test that invalid port raises URLBuilderError."""
        with pytest.raises(URLBuilderError):
            build_base_url_from_settings(
                external_url=None,
                host="localhost",
                port="not_a_number",
            )


class TestBuildFullUrl:
    """Tests for build_full_url function."""

    def test_simple_path_appending(self):
        """Test basic URL + path combination."""
        result = build_full_url("https://example.com", "/api/test")
        assert result == "https://example.com/api/test"

    def test_path_without_leading_slash_normalized(self):
        """Test that paths without leading slash get one added."""
        result = build_full_url("https://example.com", "api/test")
        assert result == "https://example.com/api/test"

    def test_base_url_trailing_slash_stripped(self):
        """Test that base URL trailing slash is removed."""
        result = build_full_url("https://example.com/", "/api/test")
        assert result == "https://example.com/api/test"

    def test_validation_disabled(self):
        """Test that validation can be disabled."""
        # This would normally fail validation (no scheme), but with validate=False it works
        result = build_full_url("example.com", "/test", validate=False)
        assert result == "example.com/test"

    def test_allowed_schemes_validation(self):
        """Test that scheme validation works."""
        # https should work with default allowed schemes
        result = build_full_url("https://example.com", "/test")
        assert result == "https://example.com/test"

    def test_disallowed_scheme_raises_error(self):
        """Test that disallowed schemes raise error."""
        with pytest.raises(URLBuilderError) as exc_info:
            build_full_url(
                "ftp://example.com",
                "/test",
                validate=True,
                allowed_schemes=["http", "https"],
            )
        assert "not in allowed schemes" in str(exc_info.value)

    def test_empty_path(self):
        """Test with empty path."""
        result = build_full_url("https://example.com", "", validate=False)
        assert result == "https://example.com/"


class TestValidateConstructedUrl:
    """Tests for validate_constructed_url function."""

    def test_valid_http_url(self):
        """Test that valid HTTP URL passes validation."""
        result = validate_constructed_url("http://example.com/path")
        assert result is True

    def test_valid_https_url(self):
        """Test that valid HTTPS URL passes validation."""
        result = validate_constructed_url("https://example.com/path")
        assert result is True

    def test_empty_url_raises_error(self):
        """Test that empty URL raises error."""
        with pytest.raises(URLBuilderError) as exc_info:
            validate_constructed_url("")
        assert "non-empty string" in str(exc_info.value)

    def test_none_url_raises_error(self):
        """Test that None URL raises error."""
        with pytest.raises(URLBuilderError) as exc_info:
            validate_constructed_url(None)
        assert "non-empty string" in str(exc_info.value)

    def test_url_without_scheme_raises_error(self):
        """Test that URL without scheme raises error."""
        with pytest.raises(URLBuilderError) as exc_info:
            validate_constructed_url("example.com/path")
        assert "scheme" in str(exc_info.value)

    def test_url_without_hostname_raises_error(self):
        """Test that URL without hostname raises error."""
        with pytest.raises(URLBuilderError) as exc_info:
            validate_constructed_url("http:///path")
        assert "hostname" in str(exc_info.value)

    def test_custom_allowed_schemes(self):
        """Test validation with custom allowed schemes."""
        result = validate_constructed_url(
            "ftp://files.example.com",
            allowed_schemes=["ftp", "sftp"],
        )
        assert result is True

    def test_scheme_not_in_allowed_raises_error(self):
        """Test that scheme not in allowed list raises error."""
        with pytest.raises(URLBuilderError) as exc_info:
            validate_constructed_url(
                "http://example.com",
                allowed_schemes=["https"],
            )
        assert "not in allowed schemes" in str(exc_info.value)

    def test_no_allowed_schemes_allows_all(self):
        """Test that None allowed_schemes allows any scheme."""
        result = validate_constructed_url(
            "custom://example.com",
            allowed_schemes=None,
        )
        assert result is True


class TestMaskSensitiveUrl:
    """Tests for mask_sensitive_url function."""

    def test_password_masked(self):
        """Test that password in URL is masked."""
        result = mask_sensitive_url("https://user:secret123@example.com/path")
        assert "secret123" not in result
        assert "***" in result

    def test_long_path_tokens_masked(self):
        """Test that long tokens in path are masked (webhook tokens)."""
        result = mask_sensitive_url(
            "https://hooks.example.com/webhook/abcdefghij1234567890abcd"
        )
        assert "abcdefghij1234567890abcd" not in result
        assert "/***" in result

    def test_query_string_masked(self):
        """Test that query strings are masked."""
        result = mask_sensitive_url(
            "https://api.example.com/data?api_key=secret"
        )
        assert "api_key=secret" not in result
        assert "?***" in result

    def test_regular_url_preserved(self):
        """Test that regular URLs without sensitive data are mostly preserved."""
        result = mask_sensitive_url("https://example.com/api/users")
        assert "example.com" in result
        assert "https://" in result
        assert "users" in result

    def test_short_path_tokens_not_masked(self):
        """Test that short path segments are not masked."""
        result = mask_sensitive_url("https://example.com/api/short")
        assert "/api/short" in result

    def test_invalid_url_returns_something(self):
        """Test that invalid URL returns something (doesn't crash)."""
        result = mask_sensitive_url("not a valid url at all")
        # Should return something without crashing - implementation may vary
        assert result is not None

    def test_url_with_port_preserved(self):
        """Test that port numbers are preserved."""
        result = mask_sensitive_url("https://example.com:8443/api")
        assert ":8443" in result

    def test_combined_sensitive_data(self):
        """Test URL with multiple sensitive elements."""
        result = mask_sensitive_url(
            "https://user:password@api.example.com/webhooks/"
            "aaaaaaaaaabbbbbbbbbbcccccccccc?token=test_value"
        )
        assert "password" not in result
        assert "aaaaaaaaaabbbbbbbbbbcccccccccc" not in result
        assert "token=test_value" not in result


class TestURLBuilderError:
    """Tests for URLBuilderError exception."""

    def test_error_is_exception(self):
        """Test that URLBuilderError is an Exception."""
        assert issubclass(URLBuilderError, Exception)

    def test_error_message_preserved(self):
        """Test that error message is preserved."""
        error = URLBuilderError("Test error message")
        assert str(error) == "Test error message"

    def test_error_can_be_raised_and_caught(self):
        """Test that error can be raised and caught properly."""
        with pytest.raises(URLBuilderError) as exc_info:
            raise URLBuilderError("Custom error")
        assert "Custom error" in str(exc_info.value)


class TestURLBuilderIntegration:
    """Integration tests combining multiple URL builder functions."""

    def test_build_and_validate_workflow(self):
        """Test typical workflow of building and validating a URL."""
        base_url = build_base_url_from_settings(
            external_url="https://myapp.example.com"
        )
        full_url = build_full_url(base_url, "/api/v1/users")

        assert validate_constructed_url(full_url)
        assert full_url == "https://myapp.example.com/api/v1/users"

    def test_build_from_host_port_and_validate(self):
        """Test building URL from host/port and validating."""
        base_url = build_base_url_from_settings(
            host="0.0.0.0",
            port=8080,
        )
        full_url = build_full_url(base_url, "/health")

        assert validate_constructed_url(full_url)
        assert "localhost" in full_url  # 0.0.0.0 should be normalized

    def test_mask_built_url_with_credentials(self):
        """Test masking a dynamically built URL with credentials."""
        base_url = "https://admin:supersecret@internal.example.com"
        full_url = build_full_url(base_url, "/api/data", validate=False)
        masked = mask_sensitive_url(full_url)

        assert "supersecret" not in masked
        assert "***" in masked
