#!/usr/bin/env python3
"""Tests for tools/security.py startup security checks."""

import os
import sys
import unittest
from unittest.mock import patch
from io import StringIO
from pathlib import Path

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

from tools.security import check_security_config, _is_default_secret


class TestSecurityCheck(unittest.TestCase):
    """Tests for check_security_config() function."""

    def test_warns_on_default_secrets_non_development(self):
        """Verify it prints warnings when env vars are default and not in development."""
        env = {
            "NOVYX_API_KEY": "novyx-dev-key-change-in-production",
            "NOVYX_JWT_SECRET": "novyx-secret-change-in-production-xxx",
            "NOVYX_ENVIRONMENT": "production",
            "NOVYX_STRICT_SECURITY": "0",
        }

        with patch.dict(os.environ, env, clear=True):
            with patch("sys.stderr", new_callable=StringIO) as mock_stderr:
                warnings = check_security_config()

                # Should return warnings
                self.assertEqual(len(warnings), 2)
                self.assertIn("NOVYX_API_KEY", warnings[0])
                self.assertIn("NOVYX_JWT_SECRET", warnings[1])

                # Should print to stderr in non-development
                stderr_output = mock_stderr.getvalue()
                self.assertIn("SECURITY WARNING", stderr_output)
                self.assertIn("NOVYX_API_KEY", stderr_output)
                self.assertIn("NOVYX_JWT_SECRET", stderr_output)

    def test_does_not_exit_unless_strict_security_enabled(self):
        """Verify it does NOT exit unless NOVYX_STRICT_SECURITY=1."""
        env = {
            "NOVYX_API_KEY": "novyx-dev-key-change-in-production",
            "NOVYX_JWT_SECRET": "short",
            "NOVYX_ENVIRONMENT": "production",
            "NOVYX_STRICT_SECURITY": "0",
        }

        with patch.dict(os.environ, env, clear=True):
            with patch("sys.stderr", new_callable=StringIO):
                # Should NOT raise SystemExit
                try:
                    warnings = check_security_config()
                    exited = False
                except SystemExit:
                    exited = True

                self.assertFalse(exited, "Should not exit when NOVYX_STRICT_SECURITY=0")
                self.assertGreater(len(warnings), 0)

    def test_exits_when_strict_security_enabled(self):
        """Verify it exits with code 1 when NOVYX_STRICT_SECURITY=1 and insecure."""
        env = {
            "NOVYX_API_KEY": "novyx-dev-key-change-in-production",
            "NOVYX_JWT_SECRET": "short",
            "NOVYX_ENVIRONMENT": "production",
            "NOVYX_STRICT_SECURITY": "1",
        }

        with patch.dict(os.environ, env, clear=True):
            with patch("sys.stderr", new_callable=StringIO):
                with self.assertRaises(SystemExit) as cm:
                    check_security_config()

                self.assertEqual(cm.exception.code, 1)

    def test_silent_when_secure_values_set(self):
        """Verify it does nothing when secure values are properly set."""
        env = {
            "NOVYX_API_KEY": "my-secure-production-api-key-12345",
            "NOVYX_JWT_SECRET": "my-very-secure-jwt-secret-that-is-long-enough-for-production",
            "NOVYX_ENVIRONMENT": "production",
            "NOVYX_STRICT_SECURITY": "1",
        }

        with patch.dict(os.environ, env, clear=True):
            with patch("sys.stderr", new_callable=StringIO) as mock_stderr:
                with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
                    # Should NOT raise SystemExit
                    try:
                        warnings = check_security_config()
                        exited = False
                    except SystemExit:
                        exited = True

                    # No warnings, no exit
                    self.assertEqual(warnings, [])
                    self.assertFalse(exited)

                    # No output
                    self.assertEqual(mock_stderr.getvalue(), "")
                    self.assertEqual(mock_stdout.getvalue(), "")

    def test_development_mode_prints_to_stdout(self):
        """Verify development mode prints gentle warnings to stdout, not stderr."""
        env = {
            "NOVYX_API_KEY": "novyx-dev-key-change-in-production",
            "NOVYX_JWT_SECRET": "short",
            "NOVYX_ENVIRONMENT": "development",
            "NOVYX_STRICT_SECURITY": "1",  # Should be ignored in dev mode
        }

        with patch.dict(os.environ, env, clear=True):
            with patch("sys.stderr", new_callable=StringIO) as mock_stderr:
                with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
                    # Should NOT exit even with strict mode (development override)
                    try:
                        warnings = check_security_config()
                        exited = False
                    except SystemExit:
                        exited = True

                    self.assertFalse(exited, "Should not exit in development mode")
                    self.assertGreater(len(warnings), 0)

                    # Should print to stdout (gentle), not stderr
                    stdout_output = mock_stdout.getvalue()
                    stderr_output = mock_stderr.getvalue()
                    self.assertIn("[DEV WARNING]", stdout_output)
                    self.assertEqual(stderr_output, "")


class TestIsDefaultSecret(unittest.TestCase):
    """Tests for _is_default_secret() helper function."""

    def test_empty_value_is_default(self):
        """Empty string should be considered default."""
        self.assertTrue(_is_default_secret(""))

    def test_short_value_is_default(self):
        """Values shorter than min_length should be considered default."""
        self.assertTrue(_is_default_secret("short", min_length=16))
        self.assertFalse(_is_default_secret("this-is-long-enough", min_length=16))

    def test_change_in_production_marker(self):
        """Values containing 'change-in-production' should be flagged."""
        self.assertTrue(_is_default_secret("novyx-dev-key-change-in-production"))
        self.assertTrue(_is_default_secret("CHANGE-IN-PRODUCTION-KEY"))
        self.assertFalse(_is_default_secret("my-secure-production-key-here"))


class TestInputValidation(unittest.TestCase):
    """Tests for input validation helpers - Phase 12 security hardening."""

    def test_validate_artifact_id_accepts_valid_uuid(self):
        """Valid UUID formats should be accepted."""
        try:
            from tools.api import validate_artifact_id
        except (ImportError, SystemExit):
            self.skipTest("API module not available")

        # Valid formats
        valid_ids = [
            "urn:uuid:12345678-1234-1234-1234-123456789abc",
            "12345678-1234-1234-1234-123456789abc",
            "urn:uuid:ABCDEF12-3456-7890-ABCD-EF1234567890",
        ]

        for artifact_id in valid_ids:
            result = validate_artifact_id(artifact_id)
            self.assertEqual(result, artifact_id)

    def test_validate_artifact_id_rejects_path_traversal(self):
        """Path traversal attempts should be rejected with 400."""
        try:
            from tools.api import validate_artifact_id
            from fastapi import HTTPException
        except (ImportError, SystemExit):
            self.skipTest("API module not available")

        # Path traversal attempts
        malicious_ids = [
            "../../../etc/passwd",
            "urn:uuid:../../secret",
            "12345678-1234-1234-1234-123456789abc/../admin",
            "..\\..\\windows\\system32",
        ]

        for artifact_id in malicious_ids:
            with self.assertRaises(HTTPException) as cm:
                validate_artifact_id(artifact_id)
            self.assertEqual(cm.exception.status_code, 400)
            self.assertIn("path traversal", cm.exception.detail.lower())

    def test_validate_artifact_id_rejects_invalid_format(self):
        """Invalid UUID formats should be rejected with 400."""
        try:
            from tools.api import validate_artifact_id
            from fastapi import HTTPException
        except (ImportError, SystemExit):
            self.skipTest("API module not available")

        # Invalid formats
        invalid_ids = [
            "not-a-uuid",
            "12345678",
            "urn:uuid:invalid",
            "<script>alert('xss')</script>",
        ]

        for artifact_id in invalid_ids:
            with self.assertRaises(HTTPException) as cm:
                validate_artifact_id(artifact_id)
            self.assertEqual(cm.exception.status_code, 400)

    def test_validate_tenant_id_accepts_valid_format(self):
        """Valid tenant ID formats should be accepted."""
        try:
            from tools.api import validate_tenant_id
        except (ImportError, SystemExit):
            self.skipTest("API module not available")

        # Valid formats
        valid_ids = [
            "acme-corp",
            "tenant_123",
            "my-tenant-name",
            "abc",  # minimum 3 chars
        ]

        for tenant_id in valid_ids:
            result = validate_tenant_id(tenant_id)
            self.assertEqual(result, tenant_id)

        # None should pass through
        self.assertIsNone(validate_tenant_id(None))

    def test_validate_tenant_id_rejects_path_traversal(self):
        """Path traversal in tenant_id should be rejected with 400."""
        try:
            from tools.api import validate_tenant_id
            from fastapi import HTTPException
        except (ImportError, SystemExit):
            self.skipTest("API module not available")

        # Path traversal attempts
        malicious_ids = [
            "../admin",
            "tenant/../secret",
            "..\\..\\etc",
        ]

        for tenant_id in malicious_ids:
            with self.assertRaises(HTTPException) as cm:
                validate_tenant_id(tenant_id)
            self.assertEqual(cm.exception.status_code, 400)


class TestAuthenticateUser(unittest.TestCase):
    """Tests for authenticate_user() - Phase 12 security hardening."""

    def test_auth_succeeds_with_valid_password_hash(self):
        """Authentication succeeds when password matches stored bcrypt hash."""
        try:
            from tools.auth import authenticate_user, hash_password, load_tenant, AUTH_AVAILABLE
            if not AUTH_AVAILABLE:
                self.skipTest("Auth dependencies not available")

            # Create mock tenant with hashed password
            test_password = "secure_password_123"
            hashed = hash_password(test_password)

            mock_tenant = {
                "tenant_id": "test-tenant",
                "users": [
                    {
                        "email": "user@test.com",
                        "password_hash": hashed,
                        "role": "admin",
                        "name": "Test User"
                    }
                ]
            }

            with patch("tools.auth.load_tenant", return_value=mock_tenant):
                result = authenticate_user("test-tenant", "user@test.com", test_password)

                self.assertIsNotNone(result)
                self.assertEqual(result["email"], "user@test.com")
                self.assertEqual(result["role"], "admin")
                self.assertEqual(result["tenant_id"], "test-tenant")

        except ImportError:
            self.skipTest("Auth module not available")

    def test_auth_fails_with_wrong_password(self):
        """Authentication fails when password does not match hash."""
        try:
            from tools.auth import authenticate_user, hash_password, AUTH_AVAILABLE
            if not AUTH_AVAILABLE:
                self.skipTest("Auth dependencies not available")

            # Create mock tenant with hashed password
            hashed = hash_password("correct_password")

            mock_tenant = {
                "tenant_id": "test-tenant",
                "users": [
                    {"email": "user@test.com", "password_hash": hashed, "role": "admin"}
                ]
            }

            with patch("tools.auth.load_tenant", return_value=mock_tenant):
                result = authenticate_user("test-tenant", "user@test.com", "wrong_password")
                self.assertIsNone(result)

        except ImportError:
            self.skipTest("Auth module not available")

    def test_auth_fails_with_missing_password_hash(self):
        """Authentication fails when user has no password_hash field."""
        try:
            from tools.auth import authenticate_user, AUTH_AVAILABLE
            if not AUTH_AVAILABLE:
                self.skipTest("Auth dependencies not available")

            # Mock tenant with user missing password_hash
            mock_tenant = {
                "tenant_id": "test-tenant",
                "users": [
                    {"email": "user@test.com", "role": "admin"}  # No password_hash!
                ]
            }

            with patch("tools.auth.load_tenant", return_value=mock_tenant):
                result = authenticate_user("test-tenant", "user@test.com", "any_password")
                self.assertIsNone(result)

        except ImportError:
            self.skipTest("Auth module not available")

    def test_auth_fails_with_nonexistent_user(self):
        """Authentication fails when user email not found in tenant."""
        try:
            from tools.auth import authenticate_user, hash_password, AUTH_AVAILABLE
            if not AUTH_AVAILABLE:
                self.skipTest("Auth dependencies not available")

            mock_tenant = {
                "tenant_id": "test-tenant",
                "users": [
                    {"email": "other@test.com", "password_hash": hash_password("pwd"), "role": "admin"}
                ]
            }

            with patch("tools.auth.load_tenant", return_value=mock_tenant):
                result = authenticate_user("test-tenant", "nonexistent@test.com", "any_password")
                self.assertIsNone(result)

        except ImportError:
            self.skipTest("Auth module not available")

    def test_auth_fails_with_nonexistent_tenant(self):
        """Authentication fails when tenant does not exist."""
        try:
            from tools.auth import authenticate_user, AUTH_AVAILABLE
            if not AUTH_AVAILABLE:
                self.skipTest("Auth dependencies not available")

            with patch("tools.auth.load_tenant", return_value=None):
                result = authenticate_user("nonexistent-tenant", "user@test.com", "password")
                self.assertIsNone(result)

        except ImportError:
            self.skipTest("Auth module not available")


if __name__ == "__main__":
    unittest.main()
