"""Unit tests for operation executor."""

from __future__ import annotations

from pathlib import Path
from typing import Any

from PIL import Image

from cascade_fm.executor import OperationExecutor
from cascade_fm.operations.base import Operation
from cascade_fm.operations.filter import FilterExtension
from cascade_fm.operations.images import CompressImage, ConvertImage, ResizeImage


class CacheableDummyTransform(Operation):
    """Dummy file-wise transform used to test incremental artifact caching."""

    execute_calls = 0

    @property
    def name(self) -> str:
        return "dummy_transform"

    @property
    def label(self) -> str:
        return "Dummy Transform"

    @property
    def description(self) -> str:
        return "Writes a transformed copy per input file"

    @property
    def caches_artifacts(self) -> bool:
        return True

    @property
    def supports_incremental_cache(self) -> bool:
        return True

    def _execute_impl(self, files: list[Path], **params: Any) -> list[Path]:
        CacheableDummyTransform.execute_calls += 1
        suffix = params.get("suffix", ".out")

        outputs: list[Path] = []
        for file_path in files:
            output = file_path.parent / f"{file_path.stem}{suffix}"
            output.write_text(file_path.read_text() + "|transformed", encoding="utf-8")
            outputs.append(output)
        return outputs


class TestOperationExecutor:
    """Tests for OperationExecutor."""

    def test_create_executor_with_default_cache(self):
        """Test executor creation with default cache."""
        executor = OperationExecutor()
        assert executor.cache is not None

    def test_execute_operation_without_cache(self, tmp_path: Path):
        """Test executing operation without cache hit."""
        executor = OperationExecutor()

        # Create test files
        file1 = tmp_path / "file1.jpg"
        file2 = tmp_path / "file2.txt"
        file1.write_text("image")
        file2.write_text("text")

        # Execute filter operation
        operation = FilterExtension()
        result = executor.execute(
            operation,
            [file1, file2],
            {"extensions": [".jpg"]},
        )

        assert result.success
        assert len(result.files) == 1
        assert result.files[0].suffix == ".jpg"

    def test_filter_operation_executes_each_time(self, tmp_path: Path, monkeypatch):
        """Test filter operations do not use artifact cache storage/restore."""
        executor = OperationExecutor()

        # Create test file
        test_file = tmp_path / "test.jpg"
        test_file.write_text("content")

        operation = FilterExtension()
        params = {"extensions": [".jpg"]}

        call_count = 0
        original_execute = operation.execute

        def counting_execute():
            nonlocal call_count
            call_count += 1
            return original_execute()

        monkeypatch.setattr(operation, "execute", counting_execute)

        result1 = executor.execute(operation, [test_file], params)
        assert result1.success
        first_call_count = call_count

        result2 = executor.execute(operation, [test_file], params)
        assert result2.success

        assert first_call_count >= 1
        assert call_count > first_call_count

    def test_filter_operation_does_not_store_manifest(self, tmp_path: Path, monkeypatch):
        """Test filter operations do not produce artifact-cache entries."""
        executor = OperationExecutor()

        test_file = tmp_path / "test.jpg"
        test_file.write_text("content")

        operation = FilterExtension()
        params = {"extensions": [".jpg"]}

        def fail_put(cache_key, output_files):
            raise AssertionError("cache.put should not be called for filter operations")

        monkeypatch.setattr(executor.cache, "put", fail_put)

        result = executor.execute(operation, [test_file], params)

        assert result.success

    def test_execute_with_invalid_params(self, tmp_path: Path):
        """Test execution with invalid parameters."""
        executor = OperationExecutor()

        test_file = tmp_path / "test.jpg"
        test_file.write_text("content")

        operation = FilterExtension()

        # Execute with invalid parameter type
        result = executor.execute(operation, [test_file], {"extensions": ".jpg"})

        assert not result.success
        assert result.error is not None
        assert "Invalid parameters" in result.error

    def test_execute_different_params_different_results(self, tmp_path: Path):
        """Test that different parameters produce different cache entries."""
        executor = OperationExecutor()

        # Create test files
        file1 = tmp_path / "file1.jpg"
        file2 = tmp_path / "file2.png"
        file1.write_text("jpg")
        file2.write_text("png")

        operation = FilterExtension()

        # Execute with .jpg filter
        result1 = executor.execute(
            operation,
            [file1, file2],
            {"extensions": [".jpg"]},
        )
        assert result1.success
        assert len(result1.files) == 1

        # Execute with .png filter - should be different result
        result2 = executor.execute(
            operation,
            [file1, file2],
            {"extensions": [".png"]},
        )
        assert result2.success
        assert len(result2.files) == 1
        assert result2.files[0].suffix == ".png"

    def test_clear_cache(self, tmp_path: Path):
        """Test clearing the cache."""
        executor = OperationExecutor()

        test_file = tmp_path / "test.jpg"
        test_file.write_text("content")

        operation = FilterExtension()
        params = {"extensions": [".jpg"]}

        # Execute and cache
        result1 = executor.execute(operation, [test_file], params)
        assert result1.success

        # Clear cache
        executor.clear_cache()

        # Execute again - should not use cache (cache was cleared)
        result2 = executor.execute(operation, [test_file], params)
        assert result2.success

    def test_multiple_operations_multiple_cache_entries(self, tmp_path: Path):
        """Test that different operations create separate cache entries."""
        from cascade_fm.operations.filter import FilterName

        executor = OperationExecutor()

        test_file = tmp_path / "test.jpg"
        test_file.write_text("content")

        # Execute two different operations
        op1 = FilterExtension()
        result1 = executor.execute(op1, [test_file], {"extensions": [".jpg"]})

        op2 = FilterName()
        result2 = executor.execute(op2, [test_file], {"pattern": "test.*"})

        assert result1.success
        assert result2.success

    def test_incremental_cache_reuses_previous_files(self, tmp_path: Path):
        """Adding one file should only process the newly added file."""
        executor = OperationExecutor()
        CacheableDummyTransform.execute_calls = 0

        file1 = tmp_path / "a.txt"
        file2 = tmp_path / "b.txt"
        file3 = tmp_path / "c.txt"
        file1.write_text("a", encoding="utf-8")
        file2.write_text("b", encoding="utf-8")
        file3.write_text("c", encoding="utf-8")

        operation = CacheableDummyTransform()
        params = {"suffix": ".resized"}

        first = executor.execute(operation, [file1, file2], params)
        assert first.success
        assert len(first.files) == 2
        assert CacheableDummyTransform.execute_calls == 2

        second = executor.execute(operation, [file1, file2], params)
        assert second.success
        assert len(second.files) == 2
        assert CacheableDummyTransform.execute_calls == 2

        third = executor.execute(operation, [file1, file2, file3], params)
        assert third.success
        assert len(third.files) == 3
        assert CacheableDummyTransform.execute_calls == 3

    def test_pipeline_smoke_filter_then_resize_cache(self, tmp_path: Path):
        """Smoke test: filter reruns, resize step reuses cache on second run."""

        class CountingFilter(FilterExtension):
            execute_impl_calls = 0

            def _execute_impl(self, files: list[Path], **params: Any) -> list[Path]:
                CountingFilter.execute_impl_calls += 1
                return super()._execute_impl(files, **params)

        class CountingResize(ResizeImage):
            execute_impl_calls = 0

            def _execute_impl(self, files: list[Path], **params: Any) -> list[Path]:
                CountingResize.execute_impl_calls += 1
                return super()._execute_impl(files, **params)

        def make_image(path: Path) -> None:
            image = Image.new("RGB", size=(80, 40), color="blue")
            image.save(path)

        executor = OperationExecutor()
        CountingFilter.execute_impl_calls = 0
        CountingResize.execute_impl_calls = 0

        file1 = tmp_path / "a.jpg"
        file2 = tmp_path / "b.jpg"
        file3 = tmp_path / "ignore.txt"
        make_image(file1)
        make_image(file2)
        file3.write_text("x", encoding="utf-8")

        all_inputs = [file1, file2, file3]

        filter_op = CountingFilter()
        resize_op = CountingResize()

        filter_result_1 = executor.execute(filter_op, all_inputs, {"extensions": [".jpg"]})
        assert filter_result_1.success
        assert len(filter_result_1.files) == 2

        resize_result_1 = executor.execute(resize_op, filter_result_1.files, {"width": 32})
        assert resize_result_1.success
        assert len(resize_result_1.files) == 2
        assert CountingResize.execute_impl_calls == 2

        filter_result_2 = executor.execute(filter_op, all_inputs, {"extensions": [".jpg"]})
        assert filter_result_2.success
        assert len(filter_result_2.files) == 2

        resize_result_2 = executor.execute(resize_op, filter_result_2.files, {"width": 32})
        assert resize_result_2.success
        assert len(resize_result_2.files) == 2

        # Filter should run again (no-cache), resize should hit cache.
        assert CountingFilter.execute_impl_calls >= 2
        assert CountingResize.execute_impl_calls == 2

    def test_pipeline_smoke_convert_then_compress_cache(self, tmp_path: Path):
        """Smoke test: convert and compress both reuse cache on second run."""

        class CountingConvert(ConvertImage):
            execute_impl_calls = 0

            def _execute_impl(self, files: list[Path], **params: Any) -> list[Path]:
                CountingConvert.execute_impl_calls += 1
                return super()._execute_impl(files, **params)

        class CountingCompress(CompressImage):
            execute_impl_calls = 0

            def _execute_impl(self, files: list[Path], **params: Any) -> list[Path]:
                CountingCompress.execute_impl_calls += 1
                return super()._execute_impl(files, **params)

        def make_image(path: Path) -> None:
            image = Image.new("RGB", size=(90, 60), color="green")
            image.save(path)

        executor = OperationExecutor()
        CountingConvert.execute_impl_calls = 0
        CountingCompress.execute_impl_calls = 0

        file1 = tmp_path / "a.jpg"
        file2 = tmp_path / "b.jpg"
        make_image(file1)
        make_image(file2)

        convert_op = CountingConvert()
        compress_op = CountingCompress()

        convert_result_1 = executor.execute(convert_op, [file1, file2], {"format": "PNG"})
        assert convert_result_1.success
        assert len(convert_result_1.files) == 2
        assert CountingConvert.execute_impl_calls == 2

        compress_result_1 = executor.execute(compress_op, convert_result_1.files, {"quality": 70})
        assert compress_result_1.success
        assert len(compress_result_1.files) == 2
        assert CountingCompress.execute_impl_calls == 2

        convert_result_2 = executor.execute(convert_op, [file1, file2], {"format": "PNG"})
        assert convert_result_2.success
        assert len(convert_result_2.files) == 2
        assert CountingConvert.execute_impl_calls == 2

        compress_result_2 = executor.execute(compress_op, convert_result_2.files, {"quality": 70})
        assert compress_result_2.success
        assert len(compress_result_2.files) == 2
        assert CountingCompress.execute_impl_calls == 2
