"""Tests for client-side input validation."""

import pytest

from baponi._base import validate_execute_params, validate_trace_id, validate_webhook_url


class TestCodeValidation:
    def test_empty_code_raises(self):
        with pytest.raises(ValueError, match="must not be empty"):
            validate_execute_params("", "python", 30, None, None)

    def test_oversized_code_raises(self):
        big_code = "x" * (1_048_576 + 1)
        with pytest.raises(ValueError, match="exceeds maximum size"):
            validate_execute_params(big_code, "python", 30, None, None)

    def test_max_size_code_passes(self):
        code = "x" * 1_048_576
        validate_execute_params(code, "python", 30, None, None)


class TestLanguageValidation:
    @pytest.mark.parametrize(
        "lang", ["python", "bash", "node"]
    )
    def test_valid_languages(self, lang: str):
        validate_execute_params("x", lang, 30, None, None)

    def test_invalid_language_raises(self):
        with pytest.raises(ValueError, match="unsupported language"):
            validate_execute_params("x", "java", 30, None, None)

    def test_case_sensitive(self):
        with pytest.raises(ValueError, match="unsupported language"):
            validate_execute_params("x", "Python", 30, None, None)


class TestTimeoutValidation:
    def test_zero_timeout_raises(self):
        with pytest.raises(ValueError, match="between 1 and 3600"):
            validate_execute_params("x", "python", 0, None, None)

    def test_negative_timeout_raises(self):
        with pytest.raises(ValueError, match="between 1 and 3600"):
            validate_execute_params("x", "python", -1, None, None)

    def test_over_max_timeout_raises(self):
        with pytest.raises(ValueError, match="between 1 and 3600"):
            validate_execute_params("x", "python", 3601, None, None)

    def test_boundary_timeouts_pass(self):
        validate_execute_params("x", "python", 1, None, None)
        validate_execute_params("x", "python", 3600, None, None)

    def test_non_int_timeout_raises(self):
        with pytest.raises(ValueError, match="must be an integer"):
            validate_execute_params("x", "python", 30.5, None, None)  # type: ignore[arg-type]


class TestThreadIdValidation:
    def test_valid_thread_ids(self):
        validate_execute_params("x", "python", 30, "abc123", None)
        validate_execute_params("x", "python", 30, "my-thread-1", None)
        validate_execute_params("x", "python", 30, "data_analysis_x8k2m9p4z1", None)

    def test_empty_thread_id_raises(self):
        with pytest.raises(ValueError, match="must not be empty"):
            validate_execute_params("x", "python", 30, "", None)

    def test_too_long_thread_id_raises(self):
        with pytest.raises(ValueError, match="exceeds maximum length"):
            validate_execute_params("x", "python", 30, "a" * 129, None)

    def test_max_length_thread_id_passes(self):
        validate_execute_params("x", "python", 30, "a" * 128, None)

    def test_invalid_start_char_raises(self):
        with pytest.raises(ValueError, match="must start with alphanumeric"):
            validate_execute_params("x", "python", 30, "-invalid", None)
        with pytest.raises(ValueError, match="must start with alphanumeric"):
            validate_execute_params("x", "python", 30, "_invalid", None)

    def test_invalid_chars_raises(self):
        with pytest.raises(ValueError, match="must start with alphanumeric"):
            validate_execute_params("x", "python", 30, "has spaces", None)
        with pytest.raises(ValueError, match="must start with alphanumeric"):
            validate_execute_params("x", "python", 30, "has.dots", None)

    def test_none_thread_id_passes(self):
        validate_execute_params("x", "python", 30, None, None)


class TestMetadataValidation:
    def test_valid_metadata(self):
        validate_execute_params("x", "python", 30, None, {"key": "value"})
        validate_execute_params(
            "x", "python", 30, None, {"user.id": "abc", "env-name": "prod"}
        )

    def test_too_many_keys_raises(self):
        meta = {f"key{i}": "val" for i in range(11)}
        with pytest.raises(ValueError, match="exceeds maximum of 10"):
            validate_execute_params("x", "python", 30, None, meta)

    def test_max_keys_passes(self):
        meta = {f"key{i}": "val" for i in range(10)}
        validate_execute_params("x", "python", 30, None, meta)

    def test_key_too_long_raises(self):
        with pytest.raises(ValueError, match="1-40 characters"):
            validate_execute_params("x", "python", 30, None, {"a" * 41: "val"})

    def test_empty_key_raises(self):
        with pytest.raises(ValueError, match="1-40 characters"):
            validate_execute_params("x", "python", 30, None, {"": "val"})

    def test_invalid_key_pattern_raises(self):
        with pytest.raises(ValueError, match="must start with alphanumeric"):
            validate_execute_params("x", "python", 30, None, {"-bad": "val"})
        with pytest.raises(ValueError, match="must start with alphanumeric"):
            validate_execute_params("x", "python", 30, None, {".bad": "val"})

    def test_value_too_long_raises(self):
        with pytest.raises(ValueError, match="exceeds maximum of 256"):
            validate_execute_params("x", "python", 30, None, {"k": "v" * 257})

    def test_non_string_value_raises(self):
        with pytest.raises(ValueError, match="must be a string"):
            validate_execute_params("x", "python", 30, None, {"k": 123})  # type: ignore[dict-item]

    def test_none_metadata_passes(self):
        validate_execute_params("x", "python", 30, None, None)


class TestEnvVarsValidation:
    def test_valid_env_vars(self):
        validate_execute_params(
            "x", "python", 30, None, None, env_vars={"MY_VAR": "value", "API_KEY": "secret"}
        )

    def test_none_env_vars_passes(self):
        validate_execute_params("x", "python", 30, None, None, env_vars=None)

    def test_empty_env_vars_passes(self):
        validate_execute_params("x", "python", 30, None, None, env_vars={})

    def test_too_many_env_vars(self):
        evars = {f"VAR_{i}": "val" for i in range(51)}
        with pytest.raises(ValueError, match="maximum 50"):
            validate_execute_params("x", "python", 30, None, None, env_vars=evars)

    def test_max_count_passes(self):
        evars = {f"VAR{i:02d}": "val" for i in range(50)}
        validate_execute_params("x", "python", 30, None, None, env_vars=evars)

    def test_lowercase_key_rejected(self):
        with pytest.raises(ValueError, match="uppercase letter"):
            validate_execute_params("x", "python", 30, None, None, env_vars={"lowercase": "v"})

    def test_mixed_case_key_rejected(self):
        with pytest.raises(ValueError, match="uppercase letters"):
            validate_execute_params("x", "python", 30, None, None, env_vars={"Mixed_Case": "v"})

    def test_key_starts_with_digit_rejected(self):
        with pytest.raises(ValueError, match="uppercase letter"):
            validate_execute_params("x", "python", 30, None, None, env_vars={"1VAR": "v"})

    def test_key_too_long(self):
        with pytest.raises(ValueError, match="1-128"):
            validate_execute_params(
                "x", "python", 30, None, None, env_vars={"A" * 129: "val"}
            )

    def test_empty_key_rejected(self):
        with pytest.raises(ValueError, match="1-128"):
            validate_execute_params("x", "python", 30, None, None, env_vars={"": "val"})

    def test_value_too_long(self):
        with pytest.raises(ValueError, match="4096"):
            validate_execute_params(
                "x", "python", 30, None, None, env_vars={"MY_VAR": "a" * 4097}
            )

    def test_non_string_value_rejected(self):
        with pytest.raises(ValueError, match="must be a string"):
            validate_execute_params(
                "x", "python", 30, None, None, env_vars={"MY_VAR": 123}  # type: ignore[dict-item]
            )

    def test_null_byte_in_value_rejected(self):
        with pytest.raises(ValueError, match="null bytes"):
            validate_execute_params(
                "x", "python", 30, None, None, env_vars={"MY_VAR": "val\x00ue"}
            )

    def test_control_char_in_value_rejected(self):
        with pytest.raises(ValueError, match="control characters"):
            validate_execute_params(
                "x", "python", 30, None, None, env_vars={"MY_VAR": "val\x01ue"}
            )

    def test_tab_and_newline_allowed(self):
        validate_execute_params(
            "x", "python", 30, None, None,
            env_vars={"MY_VAR": "line1\nline2\ttab"},
        )

    def test_reserved_name_rejected(self):
        with pytest.raises(ValueError, match="reserved by the system"):
            validate_execute_params(
                "x", "python", 30, None, None, env_vars={"PATH": "/bin"}
            )

    def test_reserved_name_home_rejected(self):
        with pytest.raises(ValueError, match="reserved by the system"):
            validate_execute_params(
                "x", "python", 30, None, None, env_vars={"HOME": "/tmp"}
            )

    def test_reserved_prefix_baponi_rejected(self):
        with pytest.raises(ValueError, match="reserved by the system"):
            validate_execute_params(
                "x", "python", 30, None, None, env_vars={"BAPONI_CUSTOM": "v"}
            )

    def test_reserved_prefix_ld_rejected(self):
        with pytest.raises(ValueError, match="reserved by the system"):
            validate_execute_params(
                "x", "python", 30, None, None, env_vars={"LD_CUSTOM": "v"}
            )

    def test_reserved_prefix_underscore_rejected(self):
        with pytest.raises(ValueError, match="uppercase letter"):
            validate_execute_params(
                "x", "python", 30, None, None, env_vars={"_HIDDEN": "v"}
            )

    def test_unicode_key_rejected(self):
        with pytest.raises(ValueError, match="ASCII"):
            validate_execute_params(
                "x", "python", 30, None, None, env_vars={"PАTH": "v"}  # Cyrillic А
            )

    def test_total_size_limit(self):
        evars = {f"VAR{i:02d}": "a" * 4000 for i in range(17)}
        with pytest.raises(ValueError, match="64KB"):
            validate_execute_params("x", "python", 30, None, None, env_vars=evars)


class TestTraceIdValidation:
    def test_valid_trace_id(self):
        validate_trace_id("trc_abc12345")

    def test_minimum_valid(self):
        validate_trace_id("trc_a")

    def test_max_length_valid(self):
        # trc_ + 20 chars = 24 total
        validate_trace_id("trc_" + "a" * 20)

    def test_with_dashes_and_underscores(self):
        validate_trace_id("trc_a-b_c-D")

    def test_missing_prefix_rejected(self):
        with pytest.raises(ValueError, match="Invalid trace_id format"):
            validate_trace_id("abc12345")

    def test_empty_string_rejected(self):
        with pytest.raises(ValueError, match="Invalid trace_id format"):
            validate_trace_id("")

    def test_prefix_only_rejected(self):
        with pytest.raises(ValueError, match="Invalid trace_id format"):
            validate_trace_id("trc_")

    def test_too_long_rejected(self):
        # trc_ + 21 chars = 25 total (exceeds 24)
        with pytest.raises(ValueError, match="Invalid trace_id format"):
            validate_trace_id("trc_" + "a" * 21)

    def test_path_traversal_rejected(self):
        with pytest.raises(ValueError, match="Invalid trace_id format"):
            validate_trace_id("../../../etc/passwd")

    def test_slash_rejected(self):
        with pytest.raises(ValueError, match="Invalid trace_id format"):
            validate_trace_id("trc_abc/def")

    def test_dot_rejected(self):
        with pytest.raises(ValueError, match="Invalid trace_id format"):
            validate_trace_id("trc_abc.def")

    def test_wrong_prefix_rejected(self):
        with pytest.raises(ValueError, match="Invalid trace_id format"):
            validate_trace_id("trx_abc12345")


class TestWebhookUrlValidation:
    def test_valid_url(self):
        validate_webhook_url("https://example.com/webhook")

    def test_http_rejected(self):
        with pytest.raises(ValueError, match="must use HTTPS"):
            validate_webhook_url("http://example.com/webhook")

    def test_bare_scheme_rejected(self):
        with pytest.raises(ValueError, match="must include a hostname"):
            validate_webhook_url("https://")

    def test_too_long_rejected(self):
        long_url = "https://example.com/" + "a" * 2040
        with pytest.raises(ValueError, match="exceeds maximum length"):
            validate_webhook_url(long_url)

    def test_max_length_passes(self):
        # Exactly 2048 chars
        url = "https://example.com/" + "a" * (2048 - len("https://example.com/"))
        validate_webhook_url(url)

    def test_empty_string_rejected(self):
        with pytest.raises(ValueError, match="must use HTTPS"):
            validate_webhook_url("")

    def test_no_scheme_rejected(self):
        with pytest.raises(ValueError, match="must use HTTPS"):
            validate_webhook_url("example.com/webhook")
