"""Tests for src/reaper_scripting/ — command protocol and ReaScriptGenerator."""

from __future__ import annotations

import ast
import sys
from pathlib import Path
import json
import tempfile

sys.path.insert(0, str(Path(__file__).parents[1]))

import pytest

from src.reaper_scripting.commands import (
    ProtocolVersionError,
    ReaScriptCommand,
    ReaScriptResult,
    read_result,
    write_command,
)
from src.reaper_scripting import ReaScriptGenerator
from src.core.schema import (
    SongDefinition,
    SongMeta,
    TrackDef,
    PluginDef,
)
from src.reaper_builder import (
    RPPBuilder,
    REAPER_BUILTINS,
    get_builtin_plugins,
)


# ------------------------------------------------------------------
# Phase 1: Command/Result Protocol
# ------------------------------------------------------------------

class TestCommandSerialization:
    """Test JSON round-trip for ReaScriptCommand."""

    def test_write_command_produces_json_file(self, tmp_path):
        cmd = ReaScriptCommand(
            version=1,
            action="calibrate",
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=120,
            track_calibration=[],
        )
        path = tmp_path / "cmd.json"
        write_command(path, cmd)
        assert path.exists()

    def test_roundtrip_command_fields(self, tmp_path):
        cmd = ReaScriptCommand(
            version=1,
            action="verify_fx",
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=60,
            track_calibration=[
                {"track_index": 0, "volume": 0.85, "pan": 0.0, "sends": []},
            ],
        )
        path = tmp_path / "cmd.json"
        write_command(path, cmd)
        text = path.read_text(encoding="utf-8")
        data = json.loads(text)
        assert data["version"] == 1
        assert data["action"] == "verify_fx"
        assert data["rpp_path"] == "C:/song.rpp"
        assert data["render_path"] == "C:/song.wav"
        assert data["timeout"] == 60
        assert len(data["track_calibration"]) == 1

    def test_read_result_roundtrip(self, tmp_path):
        cmd = ReaScriptCommand(
            version=1,
            action="calibrate",
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=120,
            track_calibration=[],
        )
        path = tmp_path / "cmd.json"
        write_command(path, cmd)
        result = read_result(path)
        assert result.version == 1
        assert result.status == "ok"
        assert result.integrated_lufs is None
        assert result.fx_errors == []


class TestVersionMismatch:
    """Test ProtocolVersionError on version mismatch."""

    def test_version_mismatch_raises(self, tmp_path):
        path = tmp_path / "bad_result.json"
        path.write_text(
            json.dumps({"version": 2, "status": "ok", "message": "", "fx_errors": []}),
            encoding="utf-8",
        )
        with pytest.raises(ProtocolVersionError) as exc_info:
            read_result(path)
        assert "expected 1, got 2" in str(exc_info.value)


class TestMissingFile:
    """Test FileNotFoundError on missing file."""

    def test_missing_command_raises_file_not_found(self):
        with pytest.raises(FileNotFoundError):
            read_result(Path("nonexistent.json"))

    def test_missing_result_raises_file_not_found(self, tmp_path):
        nonexistent = tmp_path / "does_not_exist.json"
        with pytest.raises(FileNotFoundError):
            read_result(nonexistent)


# ------------------------------------------------------------------
# Phase 2: ReaScriptGenerator
# ------------------------------------------------------------------

class TestReaScriptGeneratorOutput:
    """Test that ReaScriptGenerator produces valid Python code."""

    def test_generate_produces_file(self, tmp_path):
        cmd = ReaScriptCommand(
            version=1,
            action="calibrate",
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=120,
            track_calibration=[],
        )
        path = tmp_path / "phase2.py"
        gen = ReaScriptGenerator()
        gen.generate(path, cmd)
        assert path.exists()

    def test_generate_output_is_valid_python(self, tmp_path):
        cmd = ReaScriptCommand(
            version=1,
            action="calibrate",
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=120,
            track_calibration=[],
        )
        path = tmp_path / "phase2.py"
        gen = ReaScriptGenerator()
        gen.generate(path, cmd)
        source = path.read_text(encoding="utf-8")
        # Must not raise
        tree = ast.parse(source)
        assert tree is not None

    def test_contains_required_api_calls(self, tmp_path):
        cmd = ReaScriptCommand(
            version=1,
            action="calibrate",
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=120,
            track_calibration=[],
        )
        path = tmp_path / "phase2.py"
        gen = ReaScriptGenerator()
        gen.generate(path, cmd)
        source = path.read_text(encoding="utf-8")
        required = [
            "Main_openProject",
            "TrackFX_GetCount",
            "TrackFX_GetFXName",
            "SetMediaTrackInfo_Value",
            "CreateTrackSend",
            "Main_RenderFile",
            "CalcMediaSrcLoudness",
            "GetFunctionMetadata",
        ]
        for api in required:
            assert api in source, f"{api} not found in generated script"

    def test_script_reads_command_path(self, tmp_path):
        cmd = ReaScriptCommand(
            version=1,
            action="calibrate",
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=120,
            track_calibration=[],
        )
        path = tmp_path / "phase2.py"
        gen = ReaScriptGenerator()
        gen.generate(path, cmd)
        source = path.read_text(encoding="utf-8")
        # Must reference the command JSON path
        assert "fl_control_command.json" in source
        assert "fl_control_result.json" in source

    def test_script_has_handrolled_json_parser(self, tmp_path):
        cmd = ReaScriptCommand(
            version=1,
            action="calibrate",
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=120,
            track_calibration=[],
        )
        path = tmp_path / "phase2.py"
        gen = ReaScriptGenerator()
        gen.generate(path, cmd)
        source = path.read_text(encoding="utf-8")
        # Must NOT use Python's json module
        assert "import json" not in source
        # Must have hand-rolled parser
        assert "parse_json" in source
        assert "write_json" in source

    def test_script_includes_api_check_function(self, tmp_path):
        cmd = ReaScriptCommand(
            version=1,
            action="verify_fx",
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=120,
            track_calibration=[],
        )
        path = tmp_path / "phase2.py"
        gen = ReaScriptGenerator()
        gen.generate(path, cmd)
        source = path.read_text(encoding="utf-8")
        assert "check_api" in source
        assert "GetFunctionMetadata" in source

    def test_verify_fx_action_iterates_tracks(self, tmp_path):
        cmd = ReaScriptCommand(
            version=1,
            action="verify_fx",
            rpp_path="C:/song.rpp",
            render_path="",
            timeout=60,
            track_calibration=[],
        )
        path = tmp_path / "phase2.py"
        gen = ReaScriptGenerator()
        gen.generate(path, cmd)
        source = path.read_text(encoding="utf-8")
        # Must iterate all tracks to verify FX
        assert "CountTracks" in source
        assert "GetTrack" in source
        assert "fx_errors" in source

    def test_calibrate_action_sets_track_volume_pan(self, tmp_path):
        cmd = ReaScriptCommand(
            version=1,
            action="calibrate",
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=120,
            track_calibration=[
                {"track_index": 0, "volume": 0.85, "pan": 0.0, "sends": []},
                {
                    "track_index": 1,
                    "volume": 0.7,
                    "pan": -0.3,
                    "sends": [{"dest_track_index": 5, "level": 0.05}],
                },
            ],
        )
        path = tmp_path / "phase2.py"
        gen = ReaScriptGenerator()
        gen.generate(path, cmd)
        source = path.read_text(encoding="utf-8")
        # Volume and pan setting
        assert "D_VOL" in source
        assert "D_PAN" in source
        # Send creation
        assert "CreateTrackSend" in source

    def test_error_handling_writes_error_result(self, tmp_path):
        cmd = ReaScriptCommand(
            version=1,
            action="calibrate",
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=120,
            track_calibration=[],
        )
        path = tmp_path / "phase2.py"
        gen = ReaScriptGenerator()
        gen.generate(path, cmd)
        source = path.read_text(encoding="utf-8")
        # Must handle errors gracefully and write error status
        assert '"status": "error"' in source or "'status': 'error'" in source
        assert "write_json" in source

    def test_script_has_main_function(self, tmp_path):
        cmd = ReaScriptCommand(
            version=1,
            action="calibrate",
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=120,
            track_calibration=[],
        )
        path = tmp_path / "phase2.py"
        gen = ReaScriptGenerator()
        gen.generate(path, cmd)
        source = path.read_text(encoding="utf-8")
        assert "def main():" in source
        assert "main()" in source


# ------------------------------------------------------------------
# Phase 4: Multi-Action Dispatch, Adaptive API, JSON Round-Trip
# ------------------------------------------------------------------

class TestMultiActionDispatch:
    """4.1 Multi-action dispatch generates correct per-action functions."""

    @pytest.mark.parametrize("action_list,expected_funcs,unexpected_funcs", [
        # calibrate only → calibrate function present, render/add_plugins NOT emitted
        (["calibrate"], ["def calibrate("], ["def add_plugins(", "def configure_fx_params(", "def do_render("]),
        # add_plugins only → add_plugins and find_track present, calibrate/render NOT
        (["add_plugins"], ["def add_plugins(", "def find_track("], ["def calibrate(", "def configure_fx_params(", "def do_render("]),
        # add_plugins + verify_fx → both present, calibrate/render NOT
        (["add_plugins", "verify_fx"], ["def add_plugins(", "def verify_fx(", "def find_track("], ["def calibrate(", "def do_render("]),
        # full pipeline → all 4 action functions present
        (["add_plugins", "configure_fx_params", "calibrate", "render"],
         ["def add_plugins(", "def configure_fx_params(", "def calibrate(", "def do_render("], []),
    ])
    def test_per_action_functions_conditional(self, tmp_path, action_list, expected_funcs, unexpected_funcs):
        cmd = ReaScriptCommand(
            version=1,
            action=action_list,
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=120,
            track_calibration=[],
        )
        path = tmp_path / "phase2.py"
        gen = ReaScriptGenerator()
        gen.generate(path, cmd)
        source = path.read_text(encoding="utf-8")

        for func in expected_funcs:
            assert func in source, f"Expected '{func}' in output for actions={action_list}"

        for func in unexpected_funcs:
            assert func not in source, f"Unexpected '{func}' in output for actions={action_list}"

    def test_string_action_backward_compat(self, tmp_path):
        """String action 'calibrate' produces same output as list ['calibrate']."""
        # Generate with string
        cmd_str = ReaScriptCommand(
            version=1,
            action="calibrate",
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=120,
            track_calibration=[],
        )
        path_str = tmp_path / "str_action.py"
        gen = ReaScriptGenerator()
        gen.generate(path_str, cmd_str)
        source_str = path_str.read_text(encoding="utf-8")

        # Generate with list
        cmd_list = ReaScriptCommand(
            version=1,
            action=["calibrate"],
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=120,
            track_calibration=[],
        )
        path_list = tmp_path / "list_action.py"
        gen.generate(path_list, cmd_list)
        source_list = path_list.read_text(encoding="utf-8")

        # Both should be identical
        assert source_str == source_list, (
            "String action 'calibrate' should produce same output as list ['calibrate']"
        )

    def test_empty_action_defaults_to_calibrate(self, tmp_path):
        """Empty action list defaults to ['calibrate']."""
        cmd = ReaScriptCommand(
            version=1,
            action=[],
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=120,
            track_calibration=[],
        )
        path = tmp_path / "phase2.py"
        gen = ReaScriptGenerator()
        gen.generate(path, cmd)
        source = path.read_text(encoding="utf-8")
        assert "def calibrate(" in source
        # Render functions are NOT emitted since action defaults to ["calibrate"] only
        assert "def do_render(" not in source

    def test_dispatch_loop_present_with_multiple_actions(self, tmp_path):
        """When multiple actions present, dispatch loop iterates them."""
        cmd = ReaScriptCommand(
            version=1,
            action=["add_plugins", "verify_fx", "calibrate"],
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=120,
            track_calibration=[],
        )
        path = tmp_path / "phase2.py"
        gen = ReaScriptGenerator()
        gen.generate(path, cmd)
        source = path.read_text(encoding="utf-8")

        # Each action should appear in the dispatch if/elif chain
        assert '"add_plugins"' in source
        assert '"verify_fx"' in source
        assert '"calibrate"' in source
        # action_list variable exists
        assert "action_list" in source


class TestAdaptiveApiCheck:
    """4.2 Adaptive check_api requires FX APIs only with add_plugins."""

    def test_check_api_requires_fx_apis_with_add_plugins(self, tmp_path):
        cmd = ReaScriptCommand(
            version=1,
            action=["add_plugins"],
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=120,
            track_calibration=[],
        )
        path = tmp_path / "phase2.py"
        gen = ReaScriptGenerator()
        gen.generate(path, cmd)
        source = path.read_text(encoding="utf-8")
        assert "TrackFX_AddByName" in source
        assert "TrackFX_SetParam" in source
        assert "check_api" in source

    def test_check_api_requires_fx_apis_with_configure_fx_params(self, tmp_path):
        cmd = ReaScriptCommand(
            version=1,
            action=["add_plugins", "configure_fx_params"],
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=120,
            track_calibration=[],
        )
        path = tmp_path / "phase2.py"
        gen = ReaScriptGenerator()
        gen.generate(path, cmd)
        source = path.read_text(encoding="utf-8")
        assert "TrackFX_AddByName" in source
        assert "TrackFX_SetParam" in source

    def test_check_api_omits_fx_apis_for_calibrate_only(self, tmp_path):
        """String 'calibrate' (backward compat) should NOT include FX APIs."""
        cmd = ReaScriptCommand(
            version=1,
            action="calibrate",
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=120,
            track_calibration=[],
        )
        path = tmp_path / "phase2.py"
        gen = ReaScriptGenerator()
        gen.generate(path, cmd)
        source = path.read_text(encoding="utf-8")

        # Verify check_api exists but does NOT include FX APIs
        assert "check_api" in source
        # The check_api function defines required list; TrackFX_AddByName should NOT be in that list
        # Find the check_api function body
        check_api_start = source.find("def check_api():")
        check_api_end = source.find("def ", check_api_start + 1)
        if check_api_end == -1:
            check_api_section = source[check_api_start:]
        else:
            check_api_section = source[check_api_start:check_api_end]

        assert "TrackFX_AddByName" not in check_api_section
        assert "TrackFX_SetParam" not in check_api_section

    def test_check_api_includes_fx_apis_with_add_plugins_in_list(self, tmp_path):
        """When add_plugins is in the action list, FX APIs are required."""
        cmd = ReaScriptCommand(
            version=1,
            action=["add_plugins", "calibrate"],
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=120,
            track_calibration=[],
        )
        path = tmp_path / "phase2.py"
        gen = ReaScriptGenerator()
        gen.generate(path, cmd)
        source = path.read_text(encoding="utf-8")

        check_api_start = source.find("def check_api():")
        check_api_end = source.find("def ", check_api_start + 1)
        if check_api_end == -1:
            check_api_section = source[check_api_start:]
        else:
            check_api_section = source[check_api_start:check_api_end]

        assert "TrackFX_AddByName" in check_api_section
        assert "TrackFX_SetParam" in check_api_section


class TestPluginsToAddRoundTrip:
    """4.3 plugins_to_add/added_plugins JSON round-trip serialization."""

    def test_write_read_roundtrip_plugins_to_add(self, tmp_path):
        """Write command with plugins_to_add, read back, verify fields survive."""
        cmd = ReaScriptCommand(
            version=1,
            action=["add_plugins", "configure_fx_params", "calibrate"],
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=120,
            track_calibration=[],
            plugins_to_add=[
                {"track_name": "Bass", "fx_name": "ReaEQ", "params": {"2": 200.0, "5": 3.0}},
                {"track_name": "Lead", "fx_name": "ReaComp", "params": {"0": -18.0}},
            ],
        )
        cmd_path = tmp_path / "cmd.json"
        write_command(cmd_path, cmd)

        # Read back raw JSON
        raw = json.loads(cmd_path.read_text(encoding="utf-8"))
        assert "plugins_to_add" in raw
        assert len(raw["plugins_to_add"]) == 2
        assert raw["plugins_to_add"][0]["track_name"] == "Bass"
        assert raw["plugins_to_add"][0]["fx_name"] == "ReaEQ"
        assert raw["plugins_to_add"][0]["params"]["2"] == 200.0

    def test_read_result_includes_added_plugins(self, tmp_path):
        """Read a result JSON with added_plugins field."""
        data = {
            "version": 1,
            "status": "ok",
            "message": "",
            "fx_errors": [],
            "tracks_verified": 2,
            "added_plugins": [
                {"fx_name": "ReaEQ", "instance_id": 0, "track_name": "Bass", "status": "ok"},
                {"fx_name": "ReaComp", "instance_id": 0, "track_name": "Lead", "status": "ok"},
            ],
        }
        res_path = tmp_path / "result.json"
        res_path.write_text(json.dumps(data), encoding="utf-8")
        result = read_result(res_path)
        assert len(result.added_plugins) == 2
        assert result.added_plugins[0]["fx_name"] == "ReaEQ"
        assert result.added_plugins[0]["instance_id"] == 0
        assert result.added_plugins[1]["status"] == "ok"

    def test_write_command_omits_empty_plugins_to_add(self, tmp_path):
        """When plugins_to_add is empty, it should NOT appear in JSON."""
        cmd = ReaScriptCommand(
            version=1,
            action="calibrate",
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=120,
            track_calibration=[],
            plugins_to_add=[],
        )
        cmd_path = tmp_path / "cmd.json"
        write_command(cmd_path, cmd)
        raw = json.loads(cmd_path.read_text(encoding="utf-8"))
        assert "plugins_to_add" not in raw

    def test_read_result_handles_missing_added_plugins(self, tmp_path):
        """Old result JSON without added_plugins should default to empty list."""
        data = {
            "version": 1,
            "status": "ok",
            "message": "",
            "fx_errors": [],
            "tracks_verified": 0,
        }
        res_path = tmp_path / "result.json"
        res_path.write_text(json.dumps(data), encoding="utf-8")
        result = read_result(res_path)
        assert result.added_plugins == []


class TestBuiltinPluginSkipping:
    """4.4 builtin=True PluginDef skipped in RPP .rpp output, listed in command."""

    def test_builtin_plugin_not_in_rpp_output(self, tmp_path):
        """A PluginDef with builtin=True is excluded from .rpp VST elements."""
        song = SongDefinition(
            meta=SongMeta(bpm=95, key="Am", title="Test Builtin"),
            tracks=[
                TrackDef(
                    name="Bass",
                    volume=0.85,
                    plugins=[
                        PluginDef(
                            name="ReaEQ",
                            path="VST: ReaEQ (Cockos)",
                            index=0,
                            params={2: 200.0, 5: 3.0},
                            builtin=True,
                        ),
                        PluginDef(
                            name="Serum_2",
                            path="VST3i: Serum 2 (Xfer Records)",
                            index=1,
                            builtin=False,
                        ),
                    ],
                ),
            ],
        )
        builder = RPPBuilder(song)
        output_path = tmp_path / "test_builtin.rpp"
        builder.write(output_path)
        rpp_content = output_path.read_text(encoding="utf-8")

        # ReaEQ (builtin=True) should NOT appear in the VST chain
        assert "ReaEQ" not in rpp_content, (
            "Builtin plugin ReaEQ should NOT be written to .rpp VST section"
        )
        # Serum_2 (builtin=False) SHOULD appear
        assert "Serum 2" in rpp_content, (
            "Non-builtin plugin Serum 2 SHOULD be written to .rpp"
        )

    def test_builtin_plugin_in_get_builtin_plugins(self):
        """get_builtin_plugins() returns builtin=True plugins for the command."""
        song = SongDefinition(
            meta=SongMeta(bpm=95, key="Am", title="Test Builtin"),
            tracks=[
                TrackDef(
                    name="Bass",
                    plugins=[
                        PluginDef(
                            name="ReaEQ",
                            path="VST: ReaEQ (Cockos)",
                            index=0,
                            params={2: 200.0, 5: 3.0},
                            builtin=True,
                        ),
                        PluginDef(
                            name="ReaComp",
                            path="VST: ReaComp (Cockos)",
                            index=1,
                            params={0: -18.0},
                            builtin=False,
                        ),
                    ],
                ),
            ],
        )
        builtins = get_builtin_plugins(song)
        # ReaEQ has builtin=True → should appear
        assert len(builtins) >= 1
        reaeq_entries = [b for b in builtins if b["fx_name"] == "ReaEQ"]
        assert len(reaeq_entries) == 1
        assert reaeq_entries[0]["track_name"] == "Bass"
        # ReaComp with builtin=False BUT name in REAPER_BUILTINS → should also appear
        assert "ReaComp" in REAPER_BUILTINS
        reacomp_entries = [b for b in builtins if b["fx_name"] == "ReaComp"]
        assert len(reacomp_entries) == 1

    def test_non_builtin_not_in_reaper_builtins(self):
        """A non-builtin, non-Cockos plugin name is NOT in REAPER_BUILTINS."""
        assert "Serum_2" not in REAPER_BUILTINS
        assert "Omnisphere" not in REAPER_BUILTINS
        # But Cockos plugins ARE
        assert "ReaEQ" in REAPER_BUILTINS
        assert "ReaComp" in REAPER_BUILTINS

    def test_params_survive_in_get_builtin_plugins(self):
        """Plugin params are correctly converted to string-keyed dict."""
        song = SongDefinition(
            meta=SongMeta(bpm=95, key="Am", title="Test Params"),
            tracks=[
                TrackDef(
                    name="Bass",
                    plugins=[
                        PluginDef(
                            name="ReaEQ",
                            path="VST: ReaEQ (Cockos)",
                            index=0,
                            params={2: 200.0, 5: 3.0},
                            builtin=True,
                        ),
                    ],
                ),
            ],
        )
        builtins = get_builtin_plugins(song)
        reaeq = builtins[0]
        assert reaeq["params"] == {"2": 200.0, "5": 3.0}


# ------------------------------------------------------------------
# Phase 5: Generated Code Content Assertions
# ------------------------------------------------------------------

class TestAddPluginsSourceContent:
    """Verify add_plugins generated source contains correct ReaScript API calls."""

    @pytest.fixture
    def source_with_add_plugins(self, tmp_path: Path) -> str:
        """Generate script with add_plugins action and return source."""
        cmd = ReaScriptCommand(
            version=1,
            action=["add_plugins"],
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=120,
            track_calibration=[],
            plugins_to_add=[
                {"track_name": "Bass", "fx_name": "ReaEQ", "params": {"2": 200.0}},
            ],
        )
        path = tmp_path / "content_test.py"
        gen = ReaScriptGenerator()
        gen.generate(path, cmd)
        return path.read_text(encoding="utf-8")

    def test_add_plugins_calls_trackfx_addbyname(self, source_with_add_plugins: str):
        """Insert ReaEQ on target track — TrackFX_AddByName is called."""
        assert "RPR_TrackFX_AddByName" in source_with_add_plugins

    def test_add_plugins_handles_track_not_found(self, source_with_add_plugins: str):
        """Track not found — error written, script continues."""
        assert "track not found" in source_with_add_plugins.lower()
        assert "continue" in source_with_add_plugins

    def test_add_plugins_verifies_with_getfxname(self, source_with_add_plugins: str):
        """Plugin loaded successfully — GetFXName case-insensitive match."""
        assert "RPR_TrackFX_GetFXName" in source_with_add_plugins
        assert ".lower()" in source_with_add_plugins  # case-insensitive comparison

    def test_add_plugins_records_ok_status(self, source_with_add_plugins: str):
        """Plugin loaded — 'ok' status recorded in results."""
        assert '"status": "ok"' in source_with_add_plugins

    def test_add_plugins_records_failed_status(self, source_with_add_plugins: str):
        """Plugin failed to load — 'failed to load' status recorded."""
        assert "failed to load" in source_with_add_plugins


class TestConfigureFxParamsSourceContent:
    """Verify configure_fx_params generated source contains correct parameter setting."""

    @pytest.fixture
    def source_with_configure(self, tmp_path: Path) -> str:
        """Generate script with configure_fx_params action and return source."""
        cmd = ReaScriptCommand(
            version=1,
            action=["add_plugins", "configure_fx_params"],
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=120,
            track_calibration=[],
            plugins_to_add=[
                {"track_name": "Bass", "fx_name": "ReaEQ", "params": {"2": 200.0, "5": 3.0}},
            ],
        )
        path = tmp_path / "content_test.py"
        gen = ReaScriptGenerator()
        gen.generate(path, cmd)
        return path.read_text(encoding="utf-8")

    def test_configure_calls_setparam(self, source_with_configure: str):
        """Set ReaEQ frequency and gain — TrackFX_SetParam called."""
        assert "RPR_TrackFX_SetParam" in source_with_configure

    def test_configure_iterates_params_dict(self, source_with_configure: str):
        """Iterates params dict, converts string keys to int indices."""
        assert "int(param_idx_str)" in source_with_configure

    def test_configure_finds_fx_by_name(self, source_with_configure: str):
        """Looks up FX by name to get target index before setting params."""
        assert "RPR_TrackFX_GetCount" in source_with_configure
        assert "RPR_TrackFX_GetFXName" in source_with_configure


class TestNoBareExcept:
    """Verify generated script template has no bare except clauses."""

    def test_no_bare_except_in_generated_source(self, tmp_path: Path):
        """Generated ReaScript must not contain bare 'except:' (only 'except ...')."""
        cmd = ReaScriptCommand(
            version=1,
            action=["add_plugins", "configure_fx_params", "calibrate", "render"],
            rpp_path="C:/song.rpp",
            render_path="C:/song.wav",
            timeout=120,
            track_calibration=[],
            plugins_to_add=[
                {"track_name": "Bass", "fx_name": "ReaEQ", "params": {"2": 200.0}},
            ],
        )
        path = tmp_path / "no_bare_except.py"
        gen = ReaScriptGenerator()
        gen.generate(path, cmd)
        source = path.read_text(encoding="utf-8")

        # Check no bare "except:" (without exception type)
        lines = source.split("\n")
        for i, line in enumerate(lines):
            stripped = line.strip()
            if stripped.startswith("except:") and len(stripped) == len("except:"):
                pytest.fail(
                    f"Bare 'except:' found at line {i + 1}: {line}"
                )
