"""Tests for command parser: extracting operations from commands."""

import pytest

from rpipe.command_parser import (
    CommandParser,
    ParseError,
    UnsupportedSyntaxError,
    ParsedCommand,
)


class TestCommandParserSimple:
    """Test parsing of simple commands."""

    def test_parse_single_binary(self):
        """Parse command with just binary name."""
        parsed = CommandParser.parse("pwd")
        assert parsed.binary == "pwd"
        assert parsed.args == []
        assert parsed.input_files == []
        assert parsed.output_files == []
        assert parsed.uses_stdout is True

    def test_parse_binary_with_arguments(self):
        """Parse binary with arguments."""
        parsed = CommandParser.parse("ls -la /tmp")
        assert parsed.binary == "ls"
        assert "-la" in parsed.args
        assert "/tmp" in parsed.args

    def test_parse_cat_with_file(self):
        """Parse 'cat /var/log/syslog'."""
        parsed = CommandParser.parse("cat /var/log/syslog")
        assert parsed.binary == "cat"
        assert "/var/log/syslog" in parsed.input_files
        assert parsed.output_files == []
        assert parsed.uses_stdout is True

    def test_parse_grep_command(self):
        """Parse 'grep pattern /var/log/*'."""
        parsed = CommandParser.parse("grep error /var/log/syslog")
        assert parsed.binary == "grep"
        assert "error" in parsed.args
        assert "/var/log/syslog" in parsed.input_files

    def test_parse_echo_command(self):
        """Parse 'echo hello world'."""
        parsed = CommandParser.parse("echo hello world")
        assert parsed.binary == "echo"
        assert "hello" in parsed.args
        assert "world" in parsed.args

    def test_parse_find_command(self):
        """Parse 'find /var/log -name *.log'."""
        parsed = CommandParser.parse("find /var/log -name *.log")
        assert parsed.binary == "find"
        assert "/var/log" in parsed.args

    def test_parse_head_command(self):
        """Parse 'head -n 10 /var/log/syslog'."""
        parsed = CommandParser.parse("head -n 10 /var/log/syslog")
        assert parsed.binary == "head"
        assert "-n" in parsed.args or "10" in parsed.args

    def test_parse_tail_command(self):
        """Parse 'tail -f /var/log/syslog'."""
        parsed = CommandParser.parse("tail -f /var/log/syslog")
        assert parsed.binary == "tail"
        assert "-f" in parsed.args

    def test_parse_wc_command(self):
        """Parse 'wc -l /var/log/syslog'."""
        parsed = CommandParser.parse("wc -l /var/log/syslog")
        assert parsed.binary == "wc"
        assert "-l" in parsed.args


class TestCommandParserRedirects:
    """Test parsing of output redirects."""

    def test_parse_redirect_output(self):
        """Parse 'echo hello > /tmp/out'."""
        parsed = CommandParser.parse("echo hello > /tmp/out")
        assert parsed.binary == "echo"
        assert "hello" in parsed.args
        assert "/tmp/out" in parsed.output_files
        assert parsed.uses_stdout is False

    def test_parse_append_output(self):
        """Parse 'echo hello >> /tmp/out'."""
        parsed = CommandParser.parse("echo hello >> /tmp/out")
        assert parsed.binary == "echo"
        assert "/tmp/out" in parsed.output_files
        assert parsed.uses_stdout is False

    def test_parse_input_redirect(self):
        """Parse 'cat < /var/log/syslog'."""
        parsed = CommandParser.parse("cat < /var/log/syslog")
        assert parsed.binary == "cat"
        assert "/var/log/syslog" in parsed.input_files
        assert parsed.uses_stdin is True

    def test_parse_multiple_redirects(self):
        """Parse 'grep error /var/log/syslog > /tmp/errors'."""
        parsed = CommandParser.parse("grep error /var/log/syslog > /tmp/errors")
        assert parsed.binary == "grep"
        assert "/var/log/syslog" in parsed.input_files
        assert "/tmp/errors" in parsed.output_files

    def test_parse_cat_with_output_redirect(self):
        """Parse 'cat /var/log/syslog > /tmp/syslog.txt'."""
        parsed = CommandParser.parse("cat /var/log/syslog > /tmp/syslog.txt")
        assert parsed.binary == "cat"
        assert "/var/log/syslog" in parsed.input_files
        assert "/tmp/syslog.txt" in parsed.output_files

    def test_parse_echo_with_append(self):
        """Parse 'echo "newline" >> /tmp/log'."""
        parsed = CommandParser.parse('echo "newline" >> /tmp/log')
        assert parsed.binary == "echo"
        assert "/tmp/log" in parsed.output_files


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

    def test_reject_pipe(self):
        """Pipes should be rejected."""
        with pytest.raises(UnsupportedSyntaxError, match="pipes"):
            CommandParser.parse("cat /var/log/syslog | grep error")

    def test_reject_command_substitution_dollar_paren(self):
        """Command substitution $(...) should be rejected."""
        with pytest.raises(UnsupportedSyntaxError, match="substitution"):
            CommandParser.parse("echo $(hostname)")

    def test_reject_backtick_substitution(self):
        """Backtick substitution should be rejected."""
        with pytest.raises(UnsupportedSyntaxError, match="backtick"):
            CommandParser.parse("echo `hostname`")

    def test_reject_conditional_and(self):
        """Conditional && should be rejected."""
        with pytest.raises(UnsupportedSyntaxError, match="conditional"):
            CommandParser.parse("ls /tmp && cat /tmp/file")

    def test_reject_conditional_or(self):
        """Conditional || should be rejected."""
        with pytest.raises(UnsupportedSyntaxError):
            CommandParser.parse("test -f /tmp/file || echo missing")

    def test_reject_command_sequence(self):
        """Command sequences with ; should be rejected."""
        with pytest.raises(UnsupportedSyntaxError, match="sequence"):
            CommandParser.parse("ls /tmp; cat /tmp/file")

    def test_reject_if_then_else(self):
        """if/then/else should be rejected."""
        with pytest.raises(UnsupportedSyntaxError, match="not supported"):
            CommandParser.parse("if [ -f /tmp/file ]; then cat /tmp/file; fi")

    def test_reject_for_loop(self):
        """for loops should be rejected."""
        with pytest.raises(UnsupportedSyntaxError):
            CommandParser.parse("for f in /var/log/*; do cat $f; done")

    def test_reject_while_loop(self):
        """while loops should be rejected."""
        with pytest.raises(UnsupportedSyntaxError):
            CommandParser.parse("while true; do echo test; done")

    def test_reject_process_substitution(self):
        """Process substitution <(...) should be rejected."""
        with pytest.raises(UnsupportedSyntaxError, match="not supported"):
            CommandParser.parse("cat <(grep error /var/log/syslog)")

    def test_reject_stream_duplication(self):
        """Stream duplication >& should be rejected."""
        with pytest.raises(UnsupportedSyntaxError, match="not supported"):
            CommandParser.parse("cat /var/log/syslog 2>&1")

    def test_reject_brace_expansion(self):
        """Brace expansion should be rejected."""
        with pytest.raises(UnsupportedSyntaxError, match="not supported"):
            CommandParser.parse("cat /var/log/{syslog,auth}.log")


class TestCommandParserErrors:
    """Test error handling."""

    def test_empty_command(self):
        """Empty command should error."""
        with pytest.raises(ParseError, match="Empty"):
            CommandParser.parse("")

    def test_whitespace_only_command(self):
        """Whitespace-only command should error."""
        with pytest.raises(ParseError, match="Empty|No command"):
            CommandParser.parse("   ")

    def test_redirect_without_filename(self):
        """Redirect without filename should error."""
        with pytest.raises(ParseError, match="requires filename"):
            CommandParser.parse("echo hello >")

    def test_input_redirect_without_filename(self):
        """Input redirect without filename should error."""
        with pytest.raises(ParseError, match="requires filename"):
            CommandParser.parse("cat <")

    def test_unclosed_quote(self):
        """Unclosed quote should error."""
        with pytest.raises(ParseError, match="Invalid|No closing"):
            CommandParser.parse('echo "unclosed')

    def test_invalid_shell_syntax(self):
        """Invalid shell syntax should error."""
        with pytest.raises((ParseError, UnsupportedSyntaxError)):
            CommandParser.parse("echo  | | grep")  # Double pipe is invalid


class TestCommandParserArguments:
    """Test argument extraction and classification."""

    def test_arguments_with_flags(self):
        """Arguments with flags should be captured."""
        parsed = CommandParser.parse("ls -la -R /tmp")
        assert "-la" in parsed.args
        assert "-R" in parsed.args

    def test_arguments_with_values(self):
        """Arguments with values should be captured."""
        parsed = CommandParser.parse("head -n 20 /var/log/syslog")
        assert "-n" in parsed.args or "20" in parsed.args

    def test_glob_patterns_in_arguments(self):
        """Glob patterns should be captured as arguments."""
        parsed = CommandParser.parse("grep error /var/log/*.log")
        assert "/var/log/*.log" in parsed.input_files

    def test_relative_paths_in_arguments(self):
        """Relative paths should be captured."""
        parsed = CommandParser.parse("cat ./file.txt")
        assert "./file.txt" in parsed.input_files

    def test_current_directory_reference(self):
        """Current directory reference . should be handled."""
        parsed = CommandParser.parse("ls .")
        assert "." in parsed.args


class TestCommandParserBinaries:
    """Test binary name extraction."""

    def test_simple_binary_name(self):
        """Simple binary name should be extracted."""
        parsed = CommandParser.parse("cat /var/log/syslog")
        assert parsed.binary == "cat"

    def test_binary_with_path(self):
        """Binary with path should use last component."""
        # This depends on implementation - might be "/bin/cat" or "cat"
        parsed = CommandParser.parse("/bin/cat /var/log/syslog")
        # Binary should be the full path or last component
        assert parsed.binary in ("/bin/cat", "cat")

    def test_binary_case_sensitivity(self):
        """Binary names should preserve case."""
        parsed = CommandParser.parse("CAT /var/log/syslog")
        assert parsed.binary == "CAT"


class TestCommandParserWhitespace:
    """Test whitespace handling."""

    def test_extra_spaces_between_tokens(self):
        """Extra spaces should be handled."""
        parsed = CommandParser.parse("cat    /var/log/syslog")
        assert parsed.binary == "cat"
        assert "/var/log/syslog" in parsed.input_files

    def test_leading_whitespace(self):
        """Leading whitespace should be ignored."""
        parsed = CommandParser.parse("  cat /var/log/syslog")
        assert parsed.binary == "cat"

    def test_trailing_whitespace(self):
        """Trailing whitespace should be ignored."""
        parsed = CommandParser.parse("cat /var/log/syslog  ")
        assert parsed.binary == "cat"

    def test_tabs_and_spaces(self):
        """Tabs and spaces should be treated the same."""
        parsed = CommandParser.parse("cat\t/var/log/syslog")
        assert parsed.binary == "cat"


class TestCommandParserQuotedStrings:
    """Test handling of quoted strings."""

    def test_double_quoted_string(self):
        """Double-quoted strings should be handled."""
        parsed = CommandParser.parse('echo "hello world"')
        assert parsed.binary == "echo"
        assert "hello world" in parsed.args

    def test_single_quoted_string(self):
        """Single-quoted strings should be handled."""
        parsed = CommandParser.parse("echo 'hello world'")
        assert parsed.binary == "echo"
        assert "hello world" in parsed.args

    def test_quoted_file_path(self):
        """Quoted file paths should work."""
        parsed = CommandParser.parse('cat "/var/log/my log.txt"')
        assert parsed.binary == "cat"
        assert "/var/log/my log.txt" in parsed.input_files
