"""Tests for pipeline parsing and validation."""

import pytest

from rpipe.pipe_parser import (
    PipelineParser,
    Pipeline,
    PipeParseError,
    validate_pipeline_against_capabilities,
)
from rpipe.command_parser import UnsupportedSyntaxError
from rpipe.capability import Capability, CapabilitySet, OperationType


class TestPipelineParserSimple:
    """Test parsing of simple pipelines."""

    def test_parse_two_stage_pipeline(self):
        """Parse cat | grep."""
        pipeline = PipelineParser.parse("cat /var/log/syslog | grep error")
        assert pipeline.num_stages == 2
        assert pipeline.commands[0].binary == "cat"
        assert pipeline.commands[1].binary == "grep"

    def test_parse_three_stage_pipeline(self):
        """Parse cat | grep | wc."""
        pipeline = PipelineParser.parse("cat /var/log/syslog | grep error | wc -l")
        assert pipeline.num_stages == 3
        assert pipeline.commands[0].binary == "cat"
        assert pipeline.commands[1].binary == "grep"
        assert pipeline.commands[2].binary == "wc"

    def test_parse_single_command(self):
        """Single command without pipe should still work."""
        pipeline = PipelineParser.parse("cat /var/log/syslog")
        assert pipeline.num_stages == 1
        assert pipeline.commands[0].binary == "cat"

    def test_parse_pipeline_with_flags(self):
        """Pipeline with command flags."""
        pipeline = PipelineParser.parse("cat /var/log/syslog | grep -i error | wc -l")
        assert pipeline.num_stages == 3
        assert "-i" in pipeline.commands[1].args
        assert "-l" in pipeline.commands[2].args

    def test_parse_pipeline_with_quoted_strings(self):
        """Pipeline with quoted strings containing pipes should not split."""
        pipeline = PipelineParser.parse("grep 'error|warning' /var/log/syslog | wc -l")
        assert pipeline.num_stages == 2
        assert "error|warning" in pipeline.commands[0].args

    def test_parse_pipeline_with_whitespace(self):
        """Extra whitespace around pipes should be handled."""
        pipeline = PipelineParser.parse("cat /var/log/syslog  |  grep error  |  wc -l")
        assert pipeline.num_stages == 3

    def test_empty_pipeline_raises(self):
        """Empty pipeline should error."""
        with pytest.raises(PipeParseError):
            PipelineParser.parse("")

    def test_empty_stage_raises(self):
        """Empty stage in pipeline should error."""
        with pytest.raises(PipeParseError):
            PipelineParser.parse("cat /var/log/syslog | | wc -l")

    def test_trailing_pipe_raises(self):
        """Trailing pipe should error."""
        with pytest.raises(PipeParseError):
            PipelineParser.parse("cat /var/log/syslog |")

    def test_leading_pipe_raises(self):
        """Leading pipe should error."""
        with pytest.raises(PipeParseError):
            PipelineParser.parse("| grep error")


class TestPipelineParserUnsupportedSyntax:
    """Test rejection of unsupported syntax."""

    def test_reject_command_substitution_dollar_paren(self):
        """Command substitution not supported."""
        with pytest.raises(UnsupportedSyntaxError):
            PipelineParser.parse("cat $(find /var/log -name syslog) | grep error")

    def test_reject_backtick_substitution(self):
        """Backtick substitution not supported."""
        with pytest.raises(UnsupportedSyntaxError):
            PipelineParser.parse("cat `ls /var/log/syslog` | grep error")

    def test_reject_conditional_or_with_pipe(self):
        """Conditional || with pipes not supported."""
        with pytest.raises(UnsupportedSyntaxError):
            PipelineParser.parse("cat /var/log/syslog || echo 'not found' | wc")

    def test_reject_process_substitution_input(self):
        """Process substitution not supported."""
        with pytest.raises(UnsupportedSyntaxError):
            PipelineParser.parse("grep error <(cat /var/log/syslog) | wc -l")

    def test_reject_process_substitution_output(self):
        """Process substitution for output not supported."""
        with pytest.raises(UnsupportedSyntaxError):
            PipelineParser.parse("cat /var/log/syslog | tee >(grep error)")

    def test_reject_append_with_pipe(self):
        """Append redirect with pipes not supported (complex to validate)."""
        with pytest.raises(UnsupportedSyntaxError):
            PipelineParser.parse("cat /var/log/syslog >> /tmp/out | grep error")


class TestPipelineParserEdgeCases:
    """Test edge cases."""

    def test_pipeline_with_single_quotes(self):
        """Single-quoted strings should not be split on pipes."""
        pipeline = PipelineParser.parse("echo 'a|b|c' | wc -l")
        assert pipeline.num_stages == 2
        # shlex.split() removes quotes, so the arg is the unquoted string
        assert "a|b|c" in pipeline.commands[0].args

    def test_pipeline_with_double_quotes(self):
        """Double-quoted strings should not be split on pipes."""
        pipeline = PipelineParser.parse('echo "a|b|c" | wc -l')
        assert pipeline.num_stages == 2
        assert '"a|b|c"' in pipeline.commands[0].args or "a|b|c" in pipeline.commands[0].args

    def test_pipeline_repr(self):
        """Pipeline should have readable repr."""
        pipeline = PipelineParser.parse("cat /var/log/syslog | grep error | wc -l")
        repr_str = repr(pipeline)
        assert "Pipeline" in repr_str
        assert "3 stages" in repr_str


class TestPipelineValidation:
    """Test validation against capability sets."""

    @pytest.fixture
    def read_only_policy(self):
        """Read-only policy for testing."""
        return CapabilitySet([
            Capability(OperationType.READ_FILE, "/**/*"),
            Capability(OperationType.EXECUTE_BINARY, "cat|grep|wc|find|ls|head|tail"),
            Capability(OperationType.WRITE_STDOUT, None),
            Capability(OperationType.WRITE_STDERR, None),
        ])

    def test_simple_pipeline_allowed(self, read_only_policy):
        """Valid pipeline should pass validation."""
        validate_pipeline_against_capabilities(
            "cat /var/log/syslog | grep error | wc -l",
            read_only_policy
        )
        # No exception raised

    def test_pipeline_with_forbidden_binary(self, read_only_policy):
        """Pipeline with forbidden binary should be rejected."""
        with pytest.raises(Exception):  # PolicyError
            validate_pipeline_against_capabilities(
                "cat /var/log/syslog | rm /tmp/file",
                read_only_policy
            )

    def test_pipeline_with_forbidden_file(self):
        """Pipeline reading forbidden file should be rejected."""
        # Restricted policy: only /var/log/*, not /etc/*
        restricted_policy = CapabilitySet([
            Capability(OperationType.READ_FILE, "/var/log/**/*"),
            Capability(OperationType.EXECUTE_BINARY, "cat|grep"),
            Capability(OperationType.WRITE_STDOUT, None),
        ])
        with pytest.raises(Exception):  # PolicyError
            validate_pipeline_against_capabilities(
                "cat /etc/shadow | grep root",
                restricted_policy
            )

    def test_pipeline_with_restricted_binaries(self):
        """Pipeline with limited binaries."""
        restrictive_policy = CapabilitySet([
            Capability(OperationType.EXECUTE_BINARY, "echo|wc"),
            Capability(OperationType.WRITE_STDOUT, None),
        ])

        # This should fail: grep not in allowlist
        with pytest.raises(Exception):
            validate_pipeline_against_capabilities(
                "echo 'test' | grep test | wc -l",
                restrictive_policy
            )


class TestPipelineParsing:
    """Test the underlying parsing mechanics."""

    def test_split_by_pipe_simple(self):
        """Split by pipe with simple strings."""
        stages = PipelineParser._split_by_pipe("cat | grep | wc")
        assert len(stages) == 3
        assert stages[0] == "cat"
        assert stages[1] == "grep"
        assert stages[2] == "wc"

    def test_split_by_pipe_with_single_quotes(self):
        """Single quotes should prevent pipe splitting."""
        stages = PipelineParser._split_by_pipe("cat 'a|b' | grep")
        assert len(stages) == 2
        assert "'a|b'" in stages[0]

    def test_split_by_pipe_with_double_quotes(self):
        """Double quotes should prevent pipe splitting."""
        stages = PipelineParser._split_by_pipe('cat "a|b" | grep')
        assert len(stages) == 2
        assert '"a|b"' in stages[0] or "a|b" in stages[0]

    def test_split_by_pipe_double_pipe_raises(self):
        """Double pipe || should raise error."""
        with pytest.raises(PipeParseError):
            PipelineParser._split_by_pipe("cat || grep")

    def test_split_by_pipe_preserves_whitespace_in_args(self):
        """Whitespace in arguments should be preserved."""
        stages = PipelineParser._split_by_pipe("grep 'error message' | wc -l")
        assert "error message" in stages[0]


class TestPipelineIntegration:
    """Integration tests combining parsing and validation."""

    def test_find_pipe_xargs_pipeline(self):
        """Real-world pipeline: find | xargs grep."""
        # This would need xargs in the allowlist
        policy = CapabilitySet([
            Capability(OperationType.READ_FILE, "/**/*"),
            Capability(OperationType.EXECUTE_BINARY, "find|xargs|grep"),
            Capability(OperationType.WRITE_STDOUT, None),
        ])

        # Should parse successfully
        pipeline = PipelineParser.parse("find /var/log -name '*.log' | xargs grep error")
        assert pipeline.num_stages == 2
        assert pipeline.commands[0].binary == "find"
        assert pipeline.commands[1].binary == "xargs"

    def test_sort_uniq_pipeline(self):
        """Pipeline: sort | uniq."""
        policy = CapabilitySet([
            Capability(OperationType.READ_FILE, "/**/*"),
            Capability(OperationType.EXECUTE_BINARY, "cat|sort|uniq"),
            Capability(OperationType.WRITE_STDOUT, None),
        ])

        validate_pipeline_against_capabilities(
            "cat /var/log/syslog | sort | uniq",
            policy
        )

    def test_head_tail_pipeline(self):
        """Pipeline: cat | head | tail."""
        policy = CapabilitySet([
            Capability(OperationType.READ_FILE, "/**/*"),
            Capability(OperationType.EXECUTE_BINARY, "cat|head|tail"),
            Capability(OperationType.WRITE_STDOUT, None),
        ])

        validate_pipeline_against_capabilities(
            "cat /var/log/syslog | head -100 | tail -10",
            policy
        )
