"""Tests for validation module."""

import pytest
from fastapi import HTTPException

from api.validation import (
    check_path_traversal,
    sanitize_string,
    validate_branch_name,
    validate_github_owner,
    validate_github_repo,
    validate_pagination,
    validate_project_name,
    validate_search_query,
    validate_status,
    validate_url,
    validate_uuid,
)


class TestSanitizeString:
    """Tests for string sanitization."""

    def test_removes_null_bytes(self):
        """Test null bytes are removed."""
        assert sanitize_string("hello\x00world") == "helloworld"

    def test_normalizes_line_endings(self):
        """Test CRLF is normalized to LF."""
        assert sanitize_string("hello\r\nworld") == "hello\nworld"

    def test_trims_whitespace(self):
        """Test leading/trailing whitespace is trimmed."""
        assert sanitize_string("  hello  ") == "hello"

    def test_enforces_max_length(self):
        """Test string is truncated to max length."""
        result = sanitize_string("hello world", max_length=5)
        assert result == "hello"
        assert len(result) == 5

    def test_handles_empty_string(self):
        """Test empty string returns empty."""
        assert sanitize_string("") == ""

    def test_handles_none_string(self):
        """Test None-like value returns as-is."""
        assert sanitize_string("") == ""


class TestValidateUuid:
    """Tests for UUID validation."""

    def test_valid_uuid(self):
        """Test valid UUID passes validation."""
        result = validate_uuid("550e8400-e29b-41d4-a716-446655440000")
        assert result == "550e8400-e29b-41d4-a716-446655440000"

    def test_valid_uuid_uppercase(self):
        """Test uppercase UUID is normalized to lowercase."""
        result = validate_uuid("550E8400-E29B-41D4-A716-446655440000")
        assert result == "550e8400-e29b-41d4-a716-446655440000"

    def test_invalid_uuid_format(self):
        """Test invalid UUID format raises HTTPException."""
        with pytest.raises(HTTPException) as exc_info:
            validate_uuid("not-a-uuid")
        assert exc_info.value.status_code == 400

    def test_empty_uuid(self):
        """Test empty UUID raises HTTPException."""
        with pytest.raises(HTTPException) as exc_info:
            validate_uuid("")
        assert exc_info.value.status_code == 400

    def test_uuid_with_spaces(self):
        """Test UUID with spaces is trimmed and validated."""
        result = validate_uuid("  550e8400-e29b-41d4-a716-446655440000  ")
        assert result == "550e8400-e29b-41d4-a716-446655440000"


class TestValidateGitHubOwner:
    """Tests for GitHub owner validation."""

    def test_valid_owner(self):
        """Test valid owner passes validation."""
        assert validate_github_owner("octocat") == "octocat"
        assert validate_github_owner("my-org") == "my-org"
        assert validate_github_owner("MyOrg123") == "MyOrg123"

    def test_owner_too_long(self):
        """Test owner exceeding 39 characters is truncated."""
        long_owner = "a" * 50
        result = validate_github_owner(long_owner)
        assert len(result) <= 39

    def test_owner_with_invalid_chars(self):
        """Test owner with invalid characters raises HTTPException."""
        with pytest.raises(HTTPException) as exc_info:
            validate_github_owner("my_org!")  # underscore and exclamation
        assert exc_info.value.status_code == 400

    def test_owner_starting_with_hyphen(self):
        """Test owner starting with hyphen raises HTTPException."""
        with pytest.raises(HTTPException) as exc_info:
            validate_github_owner("-myorg")
        assert exc_info.value.status_code == 400

    def test_empty_owner(self):
        """Test empty owner raises HTTPException."""
        with pytest.raises(HTTPException) as exc_info:
            validate_github_owner("")
        assert exc_info.value.status_code == 400


class TestValidateGitHubRepo:
    """Tests for GitHub repository validation."""

    def test_valid_repo(self):
        """Test valid repo passes validation."""
        assert validate_github_repo("my-repo") == "my-repo"
        assert validate_github_repo("my_repo") == "my_repo"
        assert validate_github_repo("my.repo") == "my.repo"
        assert validate_github_repo("MyRepo123") == "MyRepo123"

    def test_repo_with_path_traversal(self):
        """Test repo with path traversal raises HTTPException."""
        with pytest.raises(HTTPException) as exc_info:
            validate_github_repo("../../../etc")
        assert exc_info.value.status_code == 400

    def test_repo_starting_with_slash(self):
        """Test repo starting with slash raises HTTPException."""
        with pytest.raises(HTTPException) as exc_info:
            validate_github_repo("/myrepo")
        assert exc_info.value.status_code == 400

    def test_empty_repo(self):
        """Test empty repo raises HTTPException."""
        with pytest.raises(HTTPException) as exc_info:
            validate_github_repo("")
        assert exc_info.value.status_code == 400


class TestValidateBranchName:
    """Tests for branch name validation."""

    def test_valid_branch(self):
        """Test valid branch passes validation."""
        assert validate_branch_name("main") == "main"
        assert validate_branch_name("feature/my-feature") == "feature/my-feature"
        assert validate_branch_name("release-1.0.0") == "release-1.0.0"

    def test_none_branch(self):
        """Test None branch returns None."""
        assert validate_branch_name(None) is None

    def test_empty_branch(self):
        """Test empty branch returns None."""
        assert validate_branch_name("") is None

    def test_branch_with_path_traversal(self):
        """Test branch with path traversal raises HTTPException."""
        with pytest.raises(HTTPException) as exc_info:
            validate_branch_name("../main")
        assert exc_info.value.status_code == 400

    def test_branch_starting_with_slash(self):
        """Test branch starting with slash raises HTTPException."""
        with pytest.raises(HTTPException) as exc_info:
            validate_branch_name("/main")
        assert exc_info.value.status_code == 400


class TestValidateProjectName:
    """Tests for project name validation."""

    def test_valid_project_name(self):
        """Test valid project name passes validation."""
        assert validate_project_name("My Project") == "My Project"
        assert validate_project_name("project-123") == "project-123"
        assert validate_project_name("Project_Name.v2") == "Project_Name.v2"

    def test_empty_project_name(self):
        """Test empty project name raises HTTPException."""
        with pytest.raises(HTTPException) as exc_info:
            validate_project_name("")
        assert exc_info.value.status_code == 400

    def test_project_name_truncated(self):
        """Test long project name is truncated."""
        long_name = "a" * 150
        result = validate_project_name(long_name)
        assert len(result) <= 100


class TestValidateSearchQuery:
    """Tests for search query validation."""

    def test_valid_search(self):
        """Test valid search passes validation."""
        assert validate_search_query("test") == "test"

    def test_none_search(self):
        """Test None search returns None."""
        assert validate_search_query(None) is None

    def test_empty_search(self):
        """Test empty search returns None."""
        assert validate_search_query("") is None

    def test_search_escapes_wildcards(self):
        """Test search escapes SQL wildcards."""
        result = validate_search_query("test%user")
        assert "\\%" in result

        result = validate_search_query("test_user")
        assert "\\_" in result

    def test_search_truncated(self):
        """Test long search is truncated."""
        long_search = "a" * 300
        result = validate_search_query(long_search)
        assert len(result) <= 200


class TestValidatePagination:
    """Tests for pagination validation."""

    def test_valid_pagination(self):
        """Test valid pagination passes validation."""
        page, per_page = validate_pagination(1, 20)
        assert page == 1
        assert per_page == 20

    def test_page_less_than_one(self):
        """Test page < 1 raises HTTPException."""
        with pytest.raises(HTTPException) as exc_info:
            validate_pagination(0, 20)
        assert exc_info.value.status_code == 400

    def test_per_page_less_than_one(self):
        """Test per_page < 1 raises HTTPException."""
        with pytest.raises(HTTPException) as exc_info:
            validate_pagination(1, 0)
        assert exc_info.value.status_code == 400

    def test_per_page_capped_at_max(self):
        """Test per_page is capped at max."""
        page, per_page = validate_pagination(1, 200, max_per_page=100)
        assert per_page == 100


class TestValidateStatus:
    """Tests for status validation."""

    def test_valid_status(self):
        """Test valid status passes validation."""
        allowed = ["PASSED", "FAILED", "PENDING"]
        assert validate_status("passed", allowed) == "PASSED"
        assert validate_status("FAILED", allowed) == "FAILED"

    def test_none_status(self):
        """Test None status returns None."""
        assert validate_status(None, ["PASSED", "FAILED"]) is None

    def test_invalid_status(self):
        """Test invalid status raises HTTPException."""
        with pytest.raises(HTTPException) as exc_info:
            validate_status("INVALID", ["PASSED", "FAILED"])
        assert exc_info.value.status_code == 400


class TestValidateUrl:
    """Tests for URL validation."""

    def test_valid_url(self):
        """Test valid URL passes validation."""
        assert validate_url("https://example.com") == "https://example.com"

    def test_localhost_blocked(self):
        """Test localhost is blocked by SSRF protection."""
        with pytest.raises(HTTPException) as exc_info:
            validate_url("http://localhost:3000")
        assert exc_info.value.status_code == 400

    def test_empty_url(self):
        """Test empty URL raises HTTPException."""
        with pytest.raises(HTTPException) as exc_info:
            validate_url("")
        assert exc_info.value.status_code == 400

    def test_url_without_protocol(self):
        """Test URL without protocol raises HTTPException."""
        with pytest.raises(HTTPException) as exc_info:
            validate_url("example.com")
        assert exc_info.value.status_code == 400

    def test_javascript_url(self):
        """Test javascript: URL raises HTTPException."""
        with pytest.raises(HTTPException) as exc_info:
            validate_url("javascript:alert(1)")
        assert exc_info.value.status_code == 400

    def test_data_url(self):
        """Test data: URL raises HTTPException."""
        with pytest.raises(HTTPException) as exc_info:
            validate_url("data:text/html,<script>alert(1)</script>")
        assert exc_info.value.status_code == 400


class TestCheckPathTraversal:
    """Tests for path traversal detection."""

    def test_safe_path(self):
        """Test safe path passes validation."""
        assert check_path_traversal("file.txt") == "file.txt"
        assert check_path_traversal("dir/file.txt") == "dir/file.txt"

    def test_empty_path(self):
        """Test empty path returns empty."""
        assert check_path_traversal("") == ""

    def test_parent_directory_traversal(self):
        """Test .. path traversal raises HTTPException."""
        with pytest.raises(HTTPException) as exc_info:
            check_path_traversal("../../../etc/passwd")
        assert exc_info.value.status_code == 400

    def test_dot_slash_traversal(self):
        """Test ./ path traversal raises HTTPException."""
        with pytest.raises(HTTPException) as exc_info:
            check_path_traversal("./hidden/file")
        assert exc_info.value.status_code == 400

    def test_encoded_traversal(self):
        """Test URL-encoded traversal raises HTTPException."""
        with pytest.raises(HTTPException) as exc_info:
            check_path_traversal("%2e%2e/etc/passwd")
        assert exc_info.value.status_code == 400

    def test_double_slash(self):
        """Test double slash raises HTTPException."""
        with pytest.raises(HTTPException) as exc_info:
            check_path_traversal("//etc/passwd")
        assert exc_info.value.status_code == 400
