"""Tests for tool_chaining module.

These tests verify the tool chaining functionality including
nuclei tag detection and chain rule evaluation.
"""

import pytest


class TestDetectNucleiTags:
    """Test nuclei tag detection from service information."""

    def test_none_os_info_does_not_crash(self):
        """detect_nuclei_tags should handle None os_info gracefully.

        When os_info is None (e.g., from multi-host nmap scans),
        the function should not crash with 'NoneType' has no attribute 'lower'.

        Regression test for multi-host nmap chain_error.
        """
        from souleyez.core.tool_chaining import detect_nuclei_tags

        # Should not raise AttributeError
        result = detect_nuclei_tags([], None)
        assert result == ""

    def test_empty_os_info_returns_empty(self):
        """Empty os_info should return empty string."""
        from souleyez.core.tool_chaining import detect_nuclei_tags

        result = detect_nuclei_tags([], "")
        assert result == ""

    def test_linux_os_detected(self):
        """Linux OS should add linux tag."""
        from souleyez.core.tool_chaining import detect_nuclei_tags

        result = detect_nuclei_tags([], "Linux Ubuntu 20.04")
        assert "linux" in result

    def test_windows_os_detected(self):
        """Windows OS should add windows and microsoft tags."""
        from souleyez.core.tool_chaining import detect_nuclei_tags

        result = detect_nuclei_tags([], "Windows Server 2019")
        assert "windows" in result
        assert "microsoft" in result

    def test_apache_service_detected(self):
        """Apache service should add apache tag."""
        from souleyez.core.tool_chaining import detect_nuclei_tags

        services = [
            {"product": "Apache httpd", "raw_version": "2.4.49", "service_name": "http"}
        ]
        result = detect_nuclei_tags(services, "")
        assert "apache" in result

    def test_wordpress_detected(self):
        """WordPress in service info should add wordpress tags."""
        from souleyez.core.tool_chaining import detect_nuclei_tags

        services = [
            {"product": "WordPress", "raw_version": "5.8", "service_name": "http"}
        ]
        result = detect_nuclei_tags(services, "")
        assert "wordpress" in result

    def test_no_services_no_os_returns_empty(self):
        """No services and no OS info should return empty."""
        from souleyez.core.tool_chaining import detect_nuclei_tags

        result = detect_nuclei_tags([], "")
        assert result == ""


class TestChainDeduplication:
    """Test chain job deduplication to prevent redundant scans."""

    def test_should_skip_duplicate_helper_exists(self):
        """The _should_skip_duplicate helper should exist."""
        from souleyez.core.tool_chaining import ToolChaining

        manager = ToolChaining()
        # Method should exist
        assert hasattr(
            manager, "_should_skip_duplicate"
        ), "ToolChaining should have _should_skip_duplicate method"

    def test_should_skip_duplicate_returns_bool(self):
        """_should_skip_duplicate should return boolean."""
        from souleyez.core.tool_chaining import ToolChaining

        manager = ToolChaining()
        # Should return False for a new tool+target combo
        result = manager._should_skip_duplicate(
            tool="nuclei",
            target="http://example.com/unique-test-url",
            engagement_id=99999,  # Non-existent engagement
        )
        assert isinstance(result, bool)

    def test_sqlmap_dedup_window_is_30_minutes(self):
        """SQLMap rule-based dedup window should be 30 minutes.

        This allows re-running sqlmap chains after 30 min while preventing
        infinite loops during active scans.
        """
        # The constant is defined inside _enqueue_commands, but we can verify
        # the module loads correctly with the constant
        from souleyez.core.tool_chaining import ToolChaining

        manager = ToolChaining()

        # 30 minutes = 1800 seconds
        EXPECTED_WINDOW = 1800
        # This is a documentation test - the actual value is in _enqueue_commands
        assert EXPECTED_WINDOW == 30 * 60, "SQLMap dedup window should be 30 minutes"

    def test_sqlmap_excluded_from_secondary_dedup_check(self):
        """SQLMap should be excluded from the _should_skip_duplicate check.

        SQLMap uses multiple phases (--dbs, --tables, --columns, --dump) that
        all target the same URL. These are NOT duplicates - they are intentional
        progressive exploitation steps.

        SQLMap deduplication is handled separately via rule_id checks in the
        main dedup logic, not by this simple tool+target check.

        Regression test for auto-chain not triggering --tables jobs after
        sqlmap --dbs finds databases.
        """
        from souleyez.core.tool_chaining import ToolChaining

        manager = ToolChaining()

        # SQLMap should always return False from _should_skip_duplicate
        # because it has its own rule_id based dedup logic
        result = manager._should_skip_duplicate(
            tool="sqlmap",
            target="http://example.com/some-injection-point",
            engagement_id=1,
        )
        assert (
            result is False
        ), "sqlmap should be excluded from _should_skip_duplicate check"


class TestSmartChainRuleIds:
    """Test that smart chains have rule_id values for tracking.

    Smart chains are auto-triggered chains that don't come from user-defined rules.
    They use negative rule_id values:
        -1: ffuf → sqlmap (dynamic endpoint)
        -2: ffuf → recursive ffuf (500 response)
        -3: sqlmap → dump (sensitive columns)
        -4: gobuster redirect retry

    Regression test for jobs #792/#793 missing rule_id.
    """

    def test_smart_chain_constants_defined(self):
        """Smart chain rule_id constants should be documented in code.

        The negative rule_id values are used inline, but this test
        documents the expected values for reference.
        """
        # These are the expected smart chain rule_id values
        SMART_FFUF_SQLMAP = -1  # ffuf → sqlmap (dynamic endpoint)
        SMART_FFUF_RECURSIVE = -2  # ffuf → recursive ffuf (500 response)
        SMART_SQLMAP_DUMP = -3  # sqlmap → dump (sensitive columns)
        SMART_GOBUSTER_RETRY = -4  # gobuster redirect retry

        # All should be negative (distinguishes from regular rules)
        assert SMART_FFUF_SQLMAP < 0
        assert SMART_FFUF_RECURSIVE < 0
        assert SMART_SQLMAP_DUMP < 0
        assert SMART_GOBUSTER_RETRY < 0

    def test_tool_chaining_module_imports(self):
        """ToolChaining module should import without errors."""
        # This verifies the rule_id additions don't break the module
        from souleyez.core.tool_chaining import ToolChaining

        manager = ToolChaining()
        assert manager is not None
