"""Tests for sketch filter parsing and evaluation.

Tests the enhanced @filter syntax supporting:
- Natural language operators (is, =, :)
- Property name shortcuts (mem, plat, tgt, brd)
- Optional colon after @filter
- Compound filters with and/or logic
"""

import tempfile
from pathlib import Path
from typing import Optional

import pytest

from ci.boards import Board
from ci.compiler.sketch_filter import (
    SketchFilter,
    _normalize_property_name,
    parse_filter_from_sketch,
    parse_oneline_filter,
    should_skip_sketch,
)


class TestNormalizePropertyName:
    """Test property name normalization."""

    def test_full_names_unchanged(self) -> None:
        """Full property names should pass through unchanged."""
        assert _normalize_property_name("memory") == "memory"
        assert _normalize_property_name("platform") == "platform"
        assert _normalize_property_name("target") == "target"
        assert _normalize_property_name("board") == "board"

    def test_shorthand_names(self) -> None:
        """Shorthand names should be expanded to full names."""
        assert _normalize_property_name("mem") == "memory"
        assert _normalize_property_name("plat") == "platform"
        assert _normalize_property_name("tgt") == "target"
        assert _normalize_property_name("brd") == "board"

    def test_case_insensitive(self) -> None:
        """Property names should be case-insensitive."""
        assert _normalize_property_name("MEM") == "memory"
        assert _normalize_property_name("PLAT") == "platform"
        assert _normalize_property_name("Memory") == "memory"
        assert _normalize_property_name("PLATFORM") == "platform"

    def test_whitespace_handling(self) -> None:
        """Should trim whitespace."""
        assert _normalize_property_name("  mem  ") == "memory"
        assert _normalize_property_name("\tplat\n") == "platform"


class TestParseOnelineFilterOperators:
    """Test one-liner filter parsing with different operator styles."""

    def test_is_operator(self) -> None:
        """Test 'is' operator."""
        result = parse_oneline_filter("(memory is large)")
        assert result is not None
        assert result.require == {"memory": ["large"]}
        assert result.exclude == {}

    def test_is_not_operator(self) -> None:
        """Test 'is not' operator."""
        result = parse_oneline_filter("(memory is not low)")
        assert result is not None
        assert result.require == {}
        assert result.exclude == {"memory": ["low"]}

    def test_equals_operator(self) -> None:
        """Test '=' operator as shorthand for 'is'."""
        result = parse_oneline_filter("(mem=large)")
        assert result is not None
        assert result.require == {"memory": ["large"]}
        assert result.exclude == {}

    def test_colon_operator(self) -> None:
        """Test ':' operator as shorthand for 'is'."""
        result = parse_oneline_filter("(mem:large)")
        assert result is not None
        assert result.require == {"memory": ["large"]}
        assert result.exclude == {}

    def test_all_operator_styles_equivalent(self) -> None:
        """All three operator styles should produce equivalent results."""
        filters = [
            "(memory is large)",
            "(mem is large)",
            "(mem=large)",
            "(mem:large)",
        ]
        results = [parse_oneline_filter(f) for f in filters]

        # All should parse successfully
        assert all(r is not None for r in results)

        # First result (reference)
        ref = results[0]

        # All should be equivalent
        for result in results[1:]:
            assert result.require == ref.require
            assert result.exclude == ref.exclude


class TestParseOnelineFilterShortcuts:
    """Test property name shortcuts in filters."""

    def test_memory_shortcut(self) -> None:
        """Test 'mem' shortcut for 'memory'."""
        result = parse_oneline_filter("(mem is large)")
        assert result is not None
        assert "memory" in result.require
        assert result.require["memory"] == ["large"]

    def test_platform_shortcut(self) -> None:
        """Test 'plat' shortcut for 'platform'."""
        result = parse_oneline_filter("(plat is esp32)")
        assert result is not None
        assert "platform" in result.require
        assert result.require["platform"] == ["esp32"]

    def test_target_shortcut(self) -> None:
        """Test 'tgt' shortcut for 'target'."""
        result = parse_oneline_filter("(tgt is ESP32S3)")
        assert result is not None
        assert "target" in result.require
        assert result.require["target"] == ["ESP32S3"]

    def test_board_shortcut(self) -> None:
        """Test 'brd' shortcut for 'board'."""
        result = parse_oneline_filter("(brd is uno)")
        assert result is not None
        assert "board" in result.require
        assert result.require["board"] == ["uno"]


class TestParseOnelineFilterCompound:
    """Test compound filters with and/or logic."""

    def test_and_logic(self) -> None:
        """Test 'and' operator combining multiple conditions."""
        result = parse_oneline_filter("(mem is large) and (plat is esp32)")
        assert result is not None
        assert result.require == {
            "memory": ["large"],
            "platform": ["esp32"],
        }

    def test_or_in_exclude(self) -> None:
        """Test 'or' logic with negation (exclude)."""
        result = parse_oneline_filter("(plat is not esp8266) and (plat is not avr)")
        assert result is not None
        assert result.exclude == {
            "platform": ["esp8266", "avr"],
        }

    def test_mixed_requires_and_excludes(self) -> None:
        """Test mixture of require and exclude conditions."""
        result = parse_oneline_filter("(mem is large) and (plat is not avr)")
        assert result is not None
        assert result.require == {"memory": ["large"]}
        assert result.exclude == {"platform": ["avr"]}

    def test_complex_filter(self) -> None:
        """Test complex filter with multiple conditions."""
        result = parse_oneline_filter(
            "(mem is large) and (plat is esp32*) and (tgt is not esp32p4)"
        )
        assert result is not None
        assert result.require == {
            "memory": ["large"],
            "platform": ["esp32*"],
        }
        assert result.exclude == {"target": ["esp32p4"]}


class TestParseOnelineFilterWildcards:
    """Test wildcard and glob patterns."""

    def test_glob_pattern(self) -> None:
        """Test glob pattern matching."""
        result = parse_oneline_filter("(plat is esp32*)")
        assert result is not None
        assert result.require == {"platform": ["esp32*"]}

    def test_cpp_define_format(self) -> None:
        """Test C++ define format (-D prefix)."""
        result = parse_oneline_filter("(tgt is -D__AVR__)")
        assert result is not None
        assert result.require == {"target": ["-D__AVR__"]}


class TestParseOnelineFilterQuotes:
    """Test quoted values in filters."""

    def test_double_quotes(self) -> None:
        """Test double-quoted values."""
        result = parse_oneline_filter('(board is "uno")')
        assert result is not None
        assert result.require == {"board": ["uno"]}

    def test_single_quotes(self) -> None:
        """Test single-quoted values."""
        result = parse_oneline_filter("(board is 'uno')")
        assert result is not None
        assert result.require == {"board": ["uno"]}


class TestParseFilterFromSketchOneliners:
    """Test parsing @filter directives from sketch files."""

    def _write_and_parse(self, sketch_content: str) -> Optional[SketchFilter]:
        """Helper to write sketch content and parse it."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".ino", delete=False) as f:
            f.write(sketch_content)
            f.flush()
            temp_path = f.name

        try:
            return parse_filter_from_sketch(Path(temp_path))
        finally:
            Path(temp_path).unlink(missing_ok=True)

    def test_filter_with_colon(self) -> None:
        """Test @filter: syntax with colon."""
        sketch_content = "// @filter: (mem is large)"
        result = self._write_and_parse(sketch_content)
        assert result is not None
        assert result.require == {"memory": ["large"]}

    def test_filter_without_colon(self) -> None:
        """Test @filter syntax without colon."""
        sketch_content = "// @filter (mem is large)"
        result = self._write_and_parse(sketch_content)
        assert result is not None
        assert result.require == {"memory": ["large"]}

    def test_filter_with_equals(self) -> None:
        """Test @filter with = operator."""
        sketch_content = "// @filter (mem=large)"
        result = self._write_and_parse(sketch_content)
        assert result is not None
        assert result.require == {"memory": ["large"]}

    def test_filter_with_colon_operator(self) -> None:
        """Test @filter with : operator."""
        sketch_content = "// @filter (mem:large)"
        result = self._write_and_parse(sketch_content)
        assert result is not None
        assert result.require == {"memory": ["large"]}

    def test_filter_compound_logic(self) -> None:
        """Test compound filter with multiple conditions."""
        sketch_content = (
            "// @filter (mem is large) and (plat is esp32*) and (tgt is not esp32p4)"
        )
        result = self._write_and_parse(sketch_content)
        assert result is not None
        assert result.require == {
            "memory": ["large"],
            "platform": ["esp32*"],
        }
        assert result.exclude == {"target": ["esp32p4"]}

    def test_no_filter(self) -> None:
        """Test sketch without @filter."""
        sketch_content = "// Regular comment\nvoid setup() {}"
        result = self._write_and_parse(sketch_content)
        assert result is None


class TestParseFilterFromSketchYAML:
    """Test parsing YAML-style multi-line @filter blocks."""

    def _write_and_parse(self, sketch_content: str) -> Optional[SketchFilter]:
        """Helper to write sketch content and parse it."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".ino", delete=False) as f:
            f.write(sketch_content)
            f.flush()
            temp_path = f.name

        try:
            return parse_filter_from_sketch(Path(temp_path))
        finally:
            Path(temp_path).unlink(missing_ok=True)

    def test_yaml_with_colon(self) -> None:
        """Test YAML-style @filter: with colon."""
        sketch_content = """// @filter:
// require:
//   - memory: large
//   - platform: esp32
// @end-filter
"""
        result = self._write_and_parse(sketch_content)
        assert result is not None
        assert result.require == {
            "memory": ["large"],
            "platform": ["esp32"],
        }

    def test_yaml_without_colon(self) -> None:
        """Test YAML-style @filter without colon."""
        sketch_content = """// @filter
// require:
//   - memory: large
//   - platform: esp32
// @end-filter
"""
        result = self._write_and_parse(sketch_content)
        assert result is not None
        assert result.require == {
            "memory": ["large"],
            "platform": ["esp32"],
        }

    def test_yaml_with_shortcuts(self) -> None:
        """Test YAML-style using property shortcuts."""
        sketch_content = """// @filter
// require:
//   - mem: large
//   - plat: esp32
// @end-filter
"""
        result = self._write_and_parse(sketch_content)
        assert result is not None
        assert result.require == {
            "memory": ["large"],
            "platform": ["esp32"],
        }

    def test_yaml_with_exclude(self) -> None:
        """Test YAML-style with exclude section."""
        sketch_content = """// @filter
// require:
//   - memory: large
// exclude:
//   - platform: avr
// @end-filter
"""
        result = self._write_and_parse(sketch_content)
        assert result is not None
        assert result.require == {"memory": ["large"]}
        assert result.exclude == {"platform": ["avr"]}


class TestShouldSkipSketch:
    """Test sketch skip logic based on board and filter."""

    @pytest.fixture
    def esp32_board(self) -> Board:
        """Create a mock ESP32 board (huge memory)."""
        from unittest.mock import MagicMock

        board = MagicMock()
        board.platform_family = "esp32"
        board.memory_class = "huge"
        board.get_mcu_target = MagicMock(return_value="ESP32")
        return board

    @pytest.fixture
    def apollo3_board(self) -> Board:
        """Create a mock Apollo3 board (large memory, not huge)."""
        from unittest.mock import MagicMock

        board = MagicMock()
        board.platform_family = "apollo3"
        board.memory_class = "large"
        board.get_mcu_target = MagicMock(return_value="Apollo3")
        return board

    @pytest.fixture
    def avr_board(self) -> Board:
        """Create a mock AVR board (Arduino Uno, low memory)."""
        from unittest.mock import MagicMock

        board = MagicMock()
        board.platform_family = "avr"
        board.memory_class = "low"
        board.get_mcu_target = MagicMock(return_value="ATmega328P")
        return board

    def test_no_filter_should_not_skip(self, esp32_board: Board) -> None:
        """Sketch without filter should not be skipped."""
        should_skip, reason = should_skip_sketch(esp32_board, None)
        assert not should_skip
        assert reason == ""

    def test_empty_filter_should_not_skip(self, esp32_board: Board) -> None:
        """Empty filter should not skip."""
        filter_obj = SketchFilter()
        should_skip, reason = should_skip_sketch(esp32_board, filter_obj)
        assert not should_skip
        assert reason == ""

    def test_require_large_memory_on_esp32(self, esp32_board: Board) -> None:
        """ESP32 (huge memory) should not be skipped for large memory requirement."""
        filter_obj = SketchFilter(require={"memory": ["large"]})
        should_skip, reason = should_skip_sketch(esp32_board, filter_obj)
        assert not should_skip

    def test_require_large_memory_on_avr(self, avr_board: Board) -> None:
        """AVR (low memory) should be skipped for large memory requirement."""
        filter_obj = SketchFilter(require={"memory": ["large"]})
        should_skip, reason = should_skip_sketch(avr_board, filter_obj)
        assert should_skip
        assert "memory" in reason

    def test_require_platform_esp32(self, esp32_board: Board, avr_board: Board) -> None:
        """ESP32 should compile, AVR should skip."""
        filter_obj = SketchFilter(require={"platform": ["esp32"]})

        should_skip_esp32, _ = should_skip_sketch(esp32_board, filter_obj)
        should_skip_avr, _ = should_skip_sketch(avr_board, filter_obj)

        assert not should_skip_esp32
        assert should_skip_avr

    def test_exclude_platform_avr(self, esp32_board: Board, avr_board: Board) -> None:
        """AVR should be excluded, ESP32 should compile."""
        filter_obj = SketchFilter(exclude={"platform": ["avr"]})

        should_skip_esp32, _ = should_skip_sketch(esp32_board, filter_obj)
        should_skip_avr, _ = should_skip_sketch(avr_board, filter_obj)

        assert not should_skip_esp32
        assert should_skip_avr

    def test_glob_pattern_platform(self, esp32_board: Board, avr_board: Board) -> None:
        """Glob pattern should match ESP32 variants."""
        filter_obj = SketchFilter(require={"platform": ["esp32*"]})

        should_skip_esp32, _ = should_skip_sketch(esp32_board, filter_obj)
        should_skip_avr, _ = should_skip_sketch(avr_board, filter_obj)

        assert not should_skip_esp32
        assert should_skip_avr


class TestEdgeCases:
    """Test edge cases and error handling."""

    def test_empty_filter_line(self) -> None:
        """Empty filter line should return None."""
        result = parse_oneline_filter("")
        assert result is None

    def test_whitespace_only(self) -> None:
        """Whitespace-only filter should return None."""
        result = parse_oneline_filter("   ")
        assert result is None

    def test_invalid_filter_syntax(self) -> None:
        """Invalid syntax should return None."""
        result = parse_oneline_filter("memory high")  # Missing parentheses
        assert result is None

    def test_unknown_property(self) -> None:
        """Unknown property should be ignored."""
        result = parse_oneline_filter("(unknown is value)")
        assert result is None  # No valid properties found

    def test_unmatched_parenthesis(self) -> None:
        """Unmatched parenthesis should be handled gracefully."""
        result = parse_oneline_filter("(memory is large")
        assert result is None  # Regex won't match

    def test_multiple_filters_only_first_used(self) -> None:
        """File with multiple @filter directives uses first match."""
        sketch_content = """
// @filter (mem is large)
some code
// @filter (plat is avr)
"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".ino", delete=False) as f:
            f.write(sketch_content)
            f.flush()
            temp_path = f.name

        try:
            result = parse_filter_from_sketch(Path(temp_path))
            assert result is not None
            # Should use the first @filter found
            assert result.require == {"memory": ["large"]}
        finally:
            Path(temp_path).unlink(missing_ok=True)

    def test_not_prefix_negation(self) -> None:
        """Test 'not' prefix for negation (e.g., 'not (board is teensylc)')."""
        # Both syntaxes should be equivalent:
        # 1. Using 'not' prefix: not (board is teensylc)
        # 2. Using 'is not' operator: (board is not teensylc)

        result1 = parse_oneline_filter("not (board is teensylc)")
        result2 = parse_oneline_filter("(board is not teensylc)")

        # Both should produce exclude condition for board=teensylc
        assert result1 is not None
        assert result2 is not None
        assert result1.exclude == {"board": ["teensylc"]}
        assert result2.exclude == {"board": ["teensylc"]}
        assert result1.require == {}
        assert result2.require == {}

    def test_not_prefix_with_and(self) -> None:
        """Test 'not' prefix in compound filter with 'and'."""
        # Filter: (platform is teensy) and not (board is teensylc)
        result = parse_oneline_filter(
            "(platform is teensy) and not (board is teensylc)"
        )

        assert result is not None
        assert result.require == {"platform": ["teensy"]}
        assert result.exclude == {"board": ["teensylc"]}

    def test_not_prefix_equivalence_with_is_not(self) -> None:
        """Test that 'not' prefix is equivalent to 'is not' operator."""
        # These three should all be equivalent
        result1 = parse_oneline_filter("not (board is teensylc)")
        result2 = parse_oneline_filter("(board is not teensylc)")
        result3 = parse_oneline_filter(
            "(platform is teensy) and not (board is teensylc)"
        )
        result4 = parse_oneline_filter(
            "(platform is teensy) and (board is not teensylc)"
        )

        # result1 and result2 should match
        assert result1.exclude == result2.exclude

        # result3 and result4 should match
        assert result3.require == result4.require
        assert result3.exclude == result4.exclude

    def test_should_skip_with_not_prefix_filter(self) -> None:
        """Test that should_skip_sketch works with 'not' prefix filters."""
        teensylc_board = Board(
            board_name="teensylc",
            platform="teensy",
            framework="arduino",
        )

        teensy41_board = Board(
            board_name="teensy41",
            platform="teensy",
            framework="arduino",
        )

        # Filter using 'not' prefix
        filter_obj = parse_oneline_filter(
            "(platform is teensy) and not (board is teensylc)"
        )

        # teensylc should be skipped
        skip_teensy_lc, reason = should_skip_sketch(teensylc_board, filter_obj)
        assert skip_teensy_lc is True
        assert "teensylc" in reason or "board" in reason

        # teensy41 should NOT be skipped
        skip_teensy_41, reason = should_skip_sketch(teensy41_board, filter_obj)
        assert skip_teensy_41 is False

    def test_case_insensitive_board_matching(self) -> None:
        """Test that board name matching is case-insensitive."""
        filter_obj = parse_oneline_filter(
            "(platform is teensy) and not (board is teensylc)"
        )

        # Test various case combinations
        test_cases = [
            ("teensylc", True, "lowercase"),
            ("TeensyLC", True, "PascalCase"),
            ("TEENSYLC", True, "UPPERCASE"),
            ("TeenSyLc", True, "MixedCase"),
            ("teensy41", False, "different board (should not skip)"),
        ]

        for board_name, should_skip_expected, description in test_cases:
            test_board = Board(
                board_name=board_name,
                platform="teensy",
                framework="arduino",
            )

            skip, _ = should_skip_sketch(test_board, filter_obj)
            assert skip == should_skip_expected, (
                f"Failed for {description}: board='{board_name}'"
            )


class TestPintestFilter:
    """Test Pintest example filter behavior."""

    def test_pintest_filter_parsing(self) -> None:
        """Test that Pintest filter parses correctly."""
        # Pintest filter: ((platform is avr) and not (board is attiny*) and not (board is atmega8*)) or (platform is teensy)
        filter_str = "((platform is avr) and not (board is attiny*) and not (board is atmega8*)) or (platform is teensy)"
        result = parse_oneline_filter(filter_str)

        assert result is not None
        # Should require avr OR teensy platforms
        assert "platform" in result.require
        assert set(result.require["platform"]) == {"avr", "teensy"}
        # Should exclude attiny* and atmega8* boards
        assert "board" in result.exclude
        assert "attiny*" in result.exclude["board"]
        assert "atmega8*" in result.exclude["board"]

    def test_pintest_atmega8a_should_skip(self) -> None:
        """Test that atmega8a should be skipped for Pintest due to code size constraints.

        The atmega8a has only 8KB flash, which is insufficient for the Pintest example
        that uses extensive template instantiation. While it matches the platform filter
        (avr and not attiny*), it should be excluded based on memory constraints.
        """
        from pathlib import Path

        from ci.boards import ATMEGA8A

        # Parse actual Pintest filter
        pintest_path = (
            Path(__file__).parent.parent.parent / "examples" / "Pintest" / "Pintest.ino"
        )
        sketch_filter = parse_filter_from_sketch(pintest_path)

        assert sketch_filter is not None, "Pintest should have a @filter directive"

        # Check atmega8a board properties
        assert ATMEGA8A.platform_family == "avr"
        assert ATMEGA8A.board_name == "atmega8a"
        assert not ATMEGA8A.board_name.startswith("attiny")

        # Test skip logic
        should_skip, reason = should_skip_sketch(ATMEGA8A, sketch_filter)

        # After fix: atmega8a should be skipped due to memory constraints
        # The filter now excludes atmega8* boards because atmega8a only has 8KB flash
        assert should_skip is True, (
            f"atmega8a should be skipped for Pintest (reason: {reason})"
        )
        assert "atmega8" in reason.lower() or "board" in reason.lower()


class TestAudioInputFilter:
    """Test AudioInput example filter behavior."""

    def test_audio_input_skips_small_teensy_boards(self) -> None:
        """AudioInput requires Teensy boards with enough RAM for PJRC Audio."""
        audio_input_path = (
            Path(__file__).parent.parent.parent
            / "examples"
            / "AudioInput"
            / "AudioInput.ino"
        )
        sketch_filter = parse_filter_from_sketch(audio_input_path)

        assert sketch_filter is not None, "AudioInput should have a @filter directive"

        cases = [
            (
                Board(board_name="teensy30", platform="teensy", framework="arduino"),
                True,
            ),
            (
                Board(board_name="teensylc", platform="teensy", framework="arduino"),
                True,
            ),
            (
                Board(board_name="teensy41", platform="teensy", framework="arduino"),
                False,
            ),
            (
                Board(board_name="esp32dev", platform="esp32", framework="arduino"),
                False,
            ),
        ]

        for board, should_skip_expected in cases:
            should_skip, reason = should_skip_sketch(board, sketch_filter)
            assert should_skip is should_skip_expected, (
                f"unexpected AudioInput filter result for {board.board_name}: {reason}"
            )


class TestBeatDetectionFilter:
    """Test BeatDetection example filter behavior."""

    def test_beat_detection_skips_small_teensy_boards(self) -> None:
        """BeatDetection uses UIAudio and should skip low-memory Teensy boards."""
        beat_detection_path = (
            Path(__file__).parent.parent.parent
            / "examples"
            / "BeatDetection"
            / "BeatDetection.ino"
        )
        sketch_filter = parse_filter_from_sketch(beat_detection_path)

        assert sketch_filter is not None, (
            "BeatDetection should have a @filter directive"
        )

        cases = [
            (
                Board(board_name="teensy30", platform="teensy", framework="arduino"),
                True,
            ),
            (
                Board(board_name="teensylc", platform="teensy", framework="arduino"),
                True,
            ),
            (
                Board(board_name="teensy41", platform="teensy", framework="arduino"),
                False,
            ),
        ]

        for board, should_skip_expected in cases:
            should_skip, reason = should_skip_sketch(board, sketch_filter)
            assert should_skip is should_skip_expected, (
                f"unexpected BeatDetection filter result for {board.board_name}: {reason}"
            )


class TestAutoResearchFilter:
    """Test AutoResearch example board filtering."""

    def test_autoresearch_filter_parsing(self) -> None:
        from ci.compiler.board_example_utils import get_example_ino_path

        sketch_filter = parse_filter_from_sketch(get_example_ino_path("AutoResearch"))

        assert sketch_filter is not None, "AutoResearch should have a @filter directive"
        assert sketch_filter.require == {
            "board": ["esp32s3", "esp32c6", "esp32p4", "teensy41", "teensy40"]
        }
        assert sketch_filter.exclude == {"memory": ["low"]}

    @pytest.mark.parametrize(
        "board_name",
        ["esp32s3", "esp32c6", "esp32p4", "teensy40", "teensy41"],
    )
    def test_autoresearch_supported_boards_compile(self, board_name: str) -> None:
        from ci.boards import create_board
        from ci.compiler.board_example_utils import should_skip_example_for_board

        should_skip, reason = should_skip_example_for_board(
            create_board(board_name), "AutoResearch"
        )

        assert not should_skip, (
            f"{board_name} should pass AutoResearch filter: {reason}"
        )

    @pytest.mark.parametrize("board_name", ["esp32dev", "uno", "teensylc"])
    def test_autoresearch_incompatible_boards_stay_filtered(
        self, board_name: str
    ) -> None:
        from ci.boards import create_board
        from ci.compiler.board_example_utils import should_skip_example_for_board

        should_skip, reason = should_skip_example_for_board(
            create_board(board_name), "AutoResearch"
        )

        assert should_skip, f"{board_name} should not pass AutoResearch filter"
        assert reason


class TestMemoryTierMatching:
    """Test three-tier memory classification: low < large < huge."""

    @pytest.fixture
    def huge_board(self) -> Board:
        """ESP32 board with huge memory."""
        from unittest.mock import MagicMock

        board = MagicMock()
        board.platform_family = "esp32"
        board.memory_class = "huge"
        board.get_mcu_target = MagicMock(return_value="ESP32")
        board.board_name = "esp32dev"
        return board

    @pytest.fixture
    def large_board(self) -> Board:
        """Apollo3 board with large (but not huge) memory."""
        from unittest.mock import MagicMock

        board = MagicMock()
        board.platform_family = "apollo3"
        board.memory_class = "large"
        board.get_mcu_target = MagicMock(return_value="Apollo3")
        board.board_name = "apollo3_red_board"
        return board

    @pytest.fixture
    def low_board(self) -> Board:
        """AVR board with low memory."""
        from unittest.mock import MagicMock

        board = MagicMock()
        board.platform_family = "avr"
        board.memory_class = "low"
        board.get_mcu_target = MagicMock(return_value="ATmega328P")
        board.board_name = "uno"
        return board

    @pytest.fixture
    def tiny_board(self) -> Board:
        """ATtiny board with tiny memory (<=1KB SRAM)."""
        from unittest.mock import MagicMock

        board = MagicMock()
        board.platform_family = "avr"
        board.memory_class = "tiny"
        board.get_mcu_target = MagicMock(return_value="ATtiny1604")
        board.board_name = "attiny1604"
        return board

    def test_require_large_matches_huge(self, huge_board: Board) -> None:
        """(memory is large) should match huge boards (ordered comparison)."""
        filter_obj = SketchFilter(require={"memory": ["large"]})
        skip, _ = should_skip_sketch(huge_board, filter_obj)
        assert not skip

    def test_require_large_matches_large(self, large_board: Board) -> None:
        """(memory is large) should match large boards."""
        filter_obj = SketchFilter(require={"memory": ["large"]})
        skip, _ = should_skip_sketch(large_board, filter_obj)
        assert not skip

    def test_require_large_skips_low(self, low_board: Board) -> None:
        """(memory is large) should skip low boards."""
        filter_obj = SketchFilter(require={"memory": ["large"]})
        skip, _ = should_skip_sketch(low_board, filter_obj)
        assert skip

    def test_require_huge_matches_huge(self, huge_board: Board) -> None:
        """(memory is huge) should match huge boards."""
        filter_obj = SketchFilter(require={"memory": ["huge"]})
        skip, _ = should_skip_sketch(huge_board, filter_obj)
        assert not skip

    def test_require_huge_skips_large(self, large_board: Board) -> None:
        """(memory is huge) should skip large boards (huge is stricter)."""
        filter_obj = SketchFilter(require={"memory": ["huge"]})
        skip, _ = should_skip_sketch(large_board, filter_obj)
        assert skip

    def test_require_huge_skips_low(self, low_board: Board) -> None:
        """(memory is huge) should skip low boards."""
        filter_obj = SketchFilter(require={"memory": ["huge"]})
        skip, _ = should_skip_sketch(low_board, filter_obj)
        assert skip

    def test_require_low_matches_low_large_huge(
        self, low_board: Board, large_board: Board, huge_board: Board
    ) -> None:
        """(memory is low) should match low/large/huge (every non-tiny board is >= low)."""
        filter_obj = SketchFilter(require={"memory": ["low"]})
        for board in [low_board, large_board, huge_board]:
            skip, _ = should_skip_sketch(board, filter_obj)
            assert not skip

    def test_require_low_skips_tiny(self, tiny_board: Board) -> None:
        """(memory is low) should skip tiny boards (tiny < low)."""
        filter_obj = SketchFilter(require={"memory": ["low"]})
        skip, _ = should_skip_sketch(tiny_board, filter_obj)
        assert skip

    def test_require_large_skips_tiny(self, tiny_board: Board) -> None:
        """(memory is large) should skip tiny boards."""
        filter_obj = SketchFilter(require={"memory": ["large"]})
        skip, _ = should_skip_sketch(tiny_board, filter_obj)
        assert skip

    def test_require_tiny_matches_all(
        self,
        tiny_board: Board,
        low_board: Board,
        large_board: Board,
        huge_board: Board,
    ) -> None:
        """(memory is tiny) should match every board (tiny is the weakest tier)."""
        filter_obj = SketchFilter(require={"memory": ["tiny"]})
        for board in [tiny_board, low_board, large_board, huge_board]:
            skip, _ = should_skip_sketch(board, filter_obj)
            assert not skip

    def test_exclude_tiny_skips_tiny_only(
        self,
        tiny_board: Board,
        low_board: Board,
        large_board: Board,
        huge_board: Board,
    ) -> None:
        """Exclude memory=tiny should skip only tiny boards (exact match)."""
        filter_obj = SketchFilter(exclude={"memory": ["tiny"]})
        skip_tiny, _ = should_skip_sketch(tiny_board, filter_obj)
        skip_low, _ = should_skip_sketch(low_board, filter_obj)
        skip_large, _ = should_skip_sketch(large_board, filter_obj)
        skip_huge, _ = should_skip_sketch(huge_board, filter_obj)
        assert skip_tiny
        assert not skip_low
        assert not skip_large
        assert not skip_huge

    def test_exclude_low_skips_low_only(
        self, low_board: Board, large_board: Board, huge_board: Board
    ) -> None:
        """Exclude memory=low should skip only low boards (exact match)."""
        filter_obj = SketchFilter(exclude={"memory": ["low"]})
        skip_low, _ = should_skip_sketch(low_board, filter_obj)
        skip_large, _ = should_skip_sketch(large_board, filter_obj)
        skip_huge, _ = should_skip_sketch(huge_board, filter_obj)
        assert skip_low
        assert not skip_large
        assert not skip_huge

    def test_exclude_huge_skips_huge_only(
        self, low_board: Board, large_board: Board, huge_board: Board
    ) -> None:
        """Exclude memory=huge should skip only huge boards (exact match)."""
        filter_obj = SketchFilter(exclude={"memory": ["huge"]})
        skip_low, _ = should_skip_sketch(low_board, filter_obj)
        skip_large, _ = should_skip_sketch(large_board, filter_obj)
        skip_huge, _ = should_skip_sketch(huge_board, filter_obj)
        assert not skip_low
        assert not skip_large
        assert skip_huge

    def test_oneliner_huge_filter(self) -> None:
        """Test parsing (memory is huge) one-liner."""
        result = parse_oneline_filter("(memory is huge)")
        assert result is not None
        assert result.require == {"memory": ["huge"]}

    def test_oneliner_huge_with_platform(self) -> None:
        """Test parsing compound filter with huge memory."""
        result = parse_oneline_filter("(memory is huge) and (platform is esp32)")
        assert result is not None
        assert result.require == {"memory": ["huge"], "platform": ["esp32"]}

    def test_huge_board_passes_large_filter(self, huge_board: Board) -> None:
        """A board with huge memory should pass (mem is large) filter."""
        filter_obj = SketchFilter(require={"memory": ["large"]})
        skip, _ = should_skip_sketch(huge_board, filter_obj)
        assert not skip, "huge memory board should pass large memory filter"


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
