#!/usr/bin/env python3
"""
Test suite for Author Whitelist Module — Security Fix for Issue #132

Tests:
- Whitelist validation
- Authorization results
- Security logging
- Configuration loading (env, config file, default)
- Edge cases (empty author, case sensitivity, etc.)
"""

import sys
import os
import json
import tempfile
import shutil
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock

# Add parent to path
sys.path.insert(0, str(Path(__file__).parent.parent))

from author_whitelist import (
    AuthorWhitelist,
    AuthorizationResult,
    SecurityLogger,
    create_403_response,
    create_200_response
)


class TestAuthorizationResult:
    """Test authorization result data structure"""
    
    def test_creation(self):
        result = AuthorizationResult(
            authorized=True,
            author="timmy",
            reason="In whitelist",
            timestamp="2026-03-30T20:00:00Z",
            issue_number=123
        )
        
        assert result.authorized is True
        assert result.author == "timmy"
        assert result.reason == "In whitelist"
        assert result.issue_number == 123
    
    def test_to_dict(self):
        result = AuthorizationResult(
            authorized=False,
            author="hacker",
            reason="Not in whitelist",
            timestamp="2026-03-30T20:00:00Z",
            issue_number=456
        )
        
        d = result.to_dict()
        assert d["authorized"] is False
        assert d["author"] == "hacker"
        assert d["issue_number"] == 456


class TestSecurityLogger:
    """Test security event logging"""
    
    def setup_method(self):
        self.temp_dir = tempfile.mkdtemp()
        self.log_dir = Path(self.temp_dir)
        self.logger = SecurityLogger(log_dir=self.log_dir)
    
    def teardown_method(self):
        shutil.rmtree(self.temp_dir)
    
    def test_log_authorization(self):
        result = AuthorizationResult(
            authorized=True,
            author="timmy",
            reason="Valid user",
            timestamp="2026-03-30T20:00:00Z",
            issue_number=123
        )
        
        self.logger.log_authorization(result, {"ip": "127.0.0.1"})
        
        # Check log file was created
        log_file = self.log_dir / "auth_events.jsonl"
        assert log_file.exists()
        
        # Check content
        with open(log_file) as f:
            entry = json.loads(f.readline())
        
        assert entry["event_type"] == "authorization"
        assert entry["authorized"] is True
        assert entry["author"] == "timmy"
        assert entry["context"]["ip"] == "127.0.0.1"
    
    def test_log_unauthorized(self):
        result = AuthorizationResult(
            authorized=False,
            author="hacker",
            reason="Not in whitelist",
            timestamp="2026-03-30T20:00:00Z",
            issue_number=456
        )
        
        self.logger.log_authorization(result)
        
        log_file = self.log_dir / "auth_events.jsonl"
        with open(log_file) as f:
            entry = json.loads(f.readline())
        
        assert entry["authorized"] is False
        assert entry["author"] == "hacker"
    
    def test_log_security_event(self):
        self.logger.log_security_event("test_event", {"detail": "value"})
        
        log_file = self.log_dir / "auth_events.jsonl"
        with open(log_file) as f:
            entry = json.loads(f.readline())
        
        assert entry["event_type"] == "test_event"
        assert entry["detail"] == "value"
        assert "timestamp" in entry


class TestAuthorWhitelist:
    """Test author whitelist validation"""
    
    def setup_method(self):
        self.temp_dir = tempfile.mkdtemp()
        self.log_dir = Path(self.temp_dir)
    
    def teardown_method(self):
        shutil.rmtree(self.temp_dir)
    
    def test_empty_whitelist_denies_all(self):
        """Secure by default: empty whitelist denies all"""
        whitelist = AuthorWhitelist(
            whitelist=[],
            log_dir=self.log_dir
        )
        
        result = whitelist.validate_author("anyone", issue_number=123)
        assert result.authorized is False
        assert result.reason == "Author not in whitelist"
    
    def test_whitelist_allows_authorized(self):
        whitelist = AuthorWhitelist(
            whitelist=["timmy", "ezra", "bezalel"],
            log_dir=self.log_dir
        )
        
        result = whitelist.validate_author("timmy", issue_number=123)
        assert result.authorized is True
        assert result.reason == "Author found in whitelist"
    
    def test_whitelist_denies_unauthorized(self):
        whitelist = AuthorWhitelist(
            whitelist=["timmy", "ezra"],
            log_dir=self.log_dir
        )
        
        result = whitelist.validate_author("hacker", issue_number=123)
        assert result.authorized is False
        assert result.reason == "Author not in whitelist"
    
    def test_case_insensitive_matching(self):
        """Usernames should be case-insensitive"""
        whitelist = AuthorWhitelist(
            whitelist=["Timmy", "EZRA"],
            log_dir=self.log_dir
        )
        
        assert whitelist.validate_author("timmy").authorized is True
        assert whitelist.validate_author("TIMMY").authorized is True
        assert whitelist.validate_author("ezra").authorized is True
        assert whitelist.validate_author("EzRa").authorized is True
    
    def test_empty_author_denied(self):
        """Empty author should be denied"""
        whitelist = AuthorWhitelist(
            whitelist=["timmy"],
            log_dir=self.log_dir
        )
        
        result = whitelist.validate_author("")
        assert result.authorized is False
        assert result.reason == "Empty author provided"
        
        result = whitelist.validate_author("   ")
        assert result.authorized is False
    
    def test_none_author_denied(self):
        """None author should be denied"""
        whitelist = AuthorWhitelist(
            whitelist=["timmy"],
            log_dir=self.log_dir
        )
        
        result = whitelist.validate_author(None)
        assert result.authorized is False
    
    def test_add_remove_author(self):
        """Test runtime modification of whitelist"""
        whitelist = AuthorWhitelist(
            whitelist=["timmy"],
            log_dir=self.log_dir
        )
        
        assert whitelist.is_authorized("newuser") is False
        
        whitelist.add_author("newuser")
        assert whitelist.is_authorized("newuser") is True
        
        whitelist.remove_author("newuser")
        assert whitelist.is_authorized("newuser") is False
    
    def test_get_whitelist(self):
        """Test getting current whitelist"""
        whitelist = AuthorWhitelist(
            whitelist=["Timmy", "EZRA"],
            log_dir=self.log_dir
        )
        
        # Should return lowercase versions
        wl = whitelist.get_whitelist()
        assert "timmy" in wl
        assert "ezra" in wl
        assert "TIMMY" not in wl  # Should be normalized to lowercase
    
    def test_is_authorized_quick_check(self):
        """Test quick authorization check without logging"""
        whitelist = AuthorWhitelist(
            whitelist=["timmy"],
            log_dir=self.log_dir
        )
        
        assert whitelist.is_authorized("timmy") is True
        assert whitelist.is_authorized("hacker") is False
        assert whitelist.is_authorized("") is False


class TestAuthorWhitelistEnvironment:
    """Test environment variable configuration"""
    
    def setup_method(self):
        self.temp_dir = tempfile.mkdtemp()
        self.log_dir = Path(self.temp_dir)
        # Store original env var
        self.original_env = os.environ.get("TIMMY_AUTHOR_WHITELIST")
    
    def teardown_method(self):
        shutil.rmtree(self.temp_dir)
        # Restore original env var
        if self.original_env is not None:
            os.environ["TIMMY_AUTHOR_WHITELIST"] = self.original_env
        elif "TIMMY_AUTHOR_WHITELIST" in os.environ:
            del os.environ["TIMMY_AUTHOR_WHITELIST"]
    
    def test_load_from_environment(self):
        """Test loading whitelist from environment variable"""
        os.environ["TIMMY_AUTHOR_WHITELIST"] = "timmy,ezra,bezalel"
        
        whitelist = AuthorWhitelist(log_dir=self.log_dir)
        
        assert whitelist.is_authorized("timmy") is True
        assert whitelist.is_authorized("ezra") is True
        assert whitelist.is_authorized("hacker") is False
    
    def test_env_var_with_spaces(self):
        """Test environment variable with spaces"""
        os.environ["TIMMY_AUTHOR_WHITELIST"] = " timmy , ezra , bezalel "
        
        whitelist = AuthorWhitelist(log_dir=self.log_dir)
        
        assert whitelist.is_authorized("timmy") is True
        assert whitelist.is_authorized("ezra") is True


class TestAuthorWhitelistConfigFile:
    """Test config file loading"""
    
    def setup_method(self):
        self.temp_dir = tempfile.mkdtemp()
        self.log_dir = Path(self.temp_dir)
        self.config_path = Path(self.temp_dir) / "config.yaml"
    
    def teardown_method(self):
        shutil.rmtree(self.temp_dir)
    
    def test_load_from_config_file(self):
        """Test loading whitelist from YAML config"""
        yaml_content = """
security:
  author_whitelist:
    - timmy
    - ezra
    - bezalel
"""
        with open(self.config_path, 'w') as f:
            f.write(yaml_content)
        
        whitelist = AuthorWhitelist(
            config_path=self.config_path,
            log_dir=self.log_dir
        )
        
        assert whitelist.is_authorized("timmy") is True
        assert whitelist.is_authorized("ezra") is True
        assert whitelist.is_authorized("hacker") is False
    
    def test_config_file_not_found(self):
        """Test handling of missing config file"""
        nonexistent_path = Path(self.temp_dir) / "nonexistent.yaml"
        
        whitelist = AuthorWhitelist(
            config_path=nonexistent_path,
            log_dir=self.log_dir
        )
        
        # Should fall back to empty list (deny all)
        assert whitelist.is_authorized("anyone") is False


class TestHTTPResponses:
    """Test HTTP-style response helpers"""
    
    def test_403_response(self):
        result = AuthorizationResult(
            authorized=False,
            author="hacker",
            reason="Not in whitelist",
            timestamp="2026-03-30T20:00:00Z",
            issue_number=123
        )
        
        response = create_403_response(result)
        
        assert response["status_code"] == 403
        assert response["error"] == "Forbidden"
        assert response["details"]["author"] == "hacker"
    
    def test_200_response(self):
        result = AuthorizationResult(
            authorized=True,
            author="timmy",
            reason="Valid user",
            timestamp="2026-03-30T20:00:00Z"
        )
        
        response = create_200_response(result)
        
        assert response["status_code"] == 200
        assert response["authorized"] is True
        assert response["author"] == "timmy"


class TestIntegrationWithTaskRouter:
    """Test integration with task router daemon"""
    
    def setup_method(self):
        self.temp_dir = tempfile.mkdtemp()
        self.log_dir = Path(self.temp_dir)
    
    def teardown_method(self):
        shutil.rmtree(self.temp_dir)
    
    def test_validate_issue_author_authorized(self):
        """Test validating issue with authorized author"""
        from task_router_daemon import ThreeHouseTaskRouter
        
        router = ThreeHouseTaskRouter(
            author_whitelist=["timmy", "ezra"],
            enforce_author_whitelist=True
        )
        
        # Mock issue with authorized author
        issue = {
            "number": 123,
            "user": {"login": "timmy"},
            "title": "Test issue"
        }
        
        assert router._validate_issue_author(issue) is True
    
    def test_validate_issue_author_unauthorized(self):
        """Test validating issue with unauthorized author"""
        from task_router_daemon import ThreeHouseTaskRouter
        
        router = ThreeHouseTaskRouter(
            author_whitelist=["timmy"],
            enforce_author_whitelist=True
        )
        
        # Mock issue with unauthorized author
        issue = {
            "number": 456,
            "user": {"login": "hacker"},
            "title": "Malicious issue"
        }
        
        assert router._validate_issue_author(issue) is False
    
    def test_validate_issue_author_whitelist_disabled(self):
        """Test that validation passes when whitelist is disabled"""
        from task_router_daemon import ThreeHouseTaskRouter
        
        router = ThreeHouseTaskRouter(
            author_whitelist=["timmy"],
            enforce_author_whitelist=False  # Disabled
        )
        
        issue = {
            "number": 789,
            "user": {"login": "anyone"},
            "title": "Test issue"
        }
        
        assert router._validate_issue_author(issue) is True
    
    def test_validate_issue_author_fallback_to_author_field(self):
        """Test fallback to 'author' field if 'user' not present"""
        from task_router_daemon import ThreeHouseTaskRouter
        
        router = ThreeHouseTaskRouter(
            author_whitelist=["timmy"],
            enforce_author_whitelist=True
        )
        
        # Issue with 'author' instead of 'user'
        issue = {
            "number": 100,
            "author": "timmy",
            "title": "Test issue"
        }
        
        assert router._validate_issue_author(issue) is True


if __name__ == "__main__":
    # Run tests with pytest if available
    import subprocess
    result = subprocess.run(
        ["python", "-m", "pytest", __file__, "-v"],
        capture_output=True,
        text=True
    )
    print(result.stdout)
    if result.stderr:
        print(result.stderr)
    exit(result.returncode)
