"""Tests for ND0401 — broken (unbalanced) n8n expression brackets."""

from pathlib import Path

import pytest

from fixtures import workflows_dir
from valn8n_lib.rules import get_rules
from valn8n_lib.rules.nd_0401 import (
    _check_expression_brackets,
    check_broken_expressions,
)
from valn8n_lib.validator import validate_workflow

FIXTURE_DIR = workflows_dir() / "test_broken_n8nexpression"


# ---------------------------------------------------------------------------
# Unit tests for _check_expression_brackets
# ---------------------------------------------------------------------------


class TestBracketChecker:
    def test_balanced_equals_prefix(self) -> None:
        assert _check_expression_brackets("={{ $json.field }}") is None

    def test_balanced_no_prefix(self) -> None:
        # Without '=' prefix, not an expression — always returns None
        assert _check_expression_brackets("{{ $json.field }}") is None

    def test_multiple_balanced(self) -> None:
        # Without '=' prefix, not an expression
        assert _check_expression_brackets("{{ a }} and {{ b }}") is None

    def test_single_braces_ignored(self) -> None:
        assert _check_expression_brackets("{ x }") is None

    def test_plain_string(self) -> None:
        assert _check_expression_brackets("hello world") is None

    def test_unclosed_no_prefix(self) -> None:
        # Without '=' prefix, not an expression — returns None
        assert _check_expression_brackets("{{ $env.FOO") is None

    def test_orphan_close(self) -> None:
        # "text }}" doesn't start with {{ so it's not an n8n expression
        assert _check_expression_brackets("text }}") is None

    def test_embedded_js_not_flagged(self) -> None:
        # Embedded JS with braces is not an n8n expression
        assert _check_expression_brackets("function() { return {{ a }}; }") is None

    def test_midstring_braces_ignored(self) -> None:
        # Braces mid-string without expression prefix are ignored
        assert _check_expression_brackets("hello {{ world") is None

    def test_unclosed_with_equals_prefix(self) -> None:
        result = _check_expression_brackets("={{ $json.field")
        assert result is not None
        assert "unclosed" in result


# ---------------------------------------------------------------------------
# Integration tests with inline workflows
# ---------------------------------------------------------------------------


def _make_workflow(parameters: dict) -> dict:
    return {
        "nodes": [
            {
                "id": "test-1",
                "name": "Test Node",
                "type": "n8n-nodes-base.noOp",
                "position": [0, 0],
                "parameters": parameters,
                "typeVersion": 1,
            },
        ],
    }


class TestCheckBrokenExpressions:
    def test_balanced_no_diagnostic(self) -> None:
        wf = _make_workflow({"field": "={{ $json.x }}"})
        diags = check_broken_expressions(wf, Path("/fake.json"))
        assert diags == []

    def test_unclosed_detected(self) -> None:
        wf = _make_workflow({"field": "={{ $env.FOO"})
        diags = check_broken_expressions(wf, Path("/fake.json"))
        assert len(diags) == 1
        assert diags[0].code.code == "ND0401"

    def test_unclosed_no_prefix_not_flagged(self) -> None:
        # Without '=' prefix, not an expression — no diagnostic
        wf = _make_workflow({"field": "{{ $env.FOO"})
        diags = check_broken_expressions(wf, Path("/fake.json"))
        assert diags == []

    def test_orphan_close_not_flagged(self) -> None:
        # "text }}" is not an n8n expression, so no diagnostic
        wf = _make_workflow({"field": "text }}"})
        diags = check_broken_expressions(wf, Path("/fake.json"))
        assert diags == []

    def test_nested_dict_broken(self) -> None:
        wf = _make_workflow({"outer": {"inner": "={{ broken"}})
        diags = check_broken_expressions(wf, Path("/fake.json"))
        assert len(diags) == 1
        assert "outer/inner" in (diags[0].location.field_path if diags[0].location else "")

    def test_list_element_broken(self) -> None:
        wf = _make_workflow({"items": ["={{ ok }}", "={{ broken"]})
        diags = check_broken_expressions(wf, Path("/fake.json"))
        assert len(diags) == 1
        assert "items/1" in (diags[0].location.field_path if diags[0].location else "")

    def test_credentials_not_checked(self) -> None:
        """Broken expressions in credentials should NOT be flagged."""
        wf = {
            "nodes": [
                {
                    "id": "test-1",
                    "name": "Test Node",
                    "type": "n8n-nodes-base.noOp",
                    "position": [0, 0],
                    "parameters": {},
                    "credentials": {"apiKey": {"id": "{{ broken"}},
                    "typeVersion": 1,
                },
            ],
        }
        diags = check_broken_expressions(wf, Path("/fake.json"))
        assert diags == []


# ---------------------------------------------------------------------------
# Fixture-driven test
# ---------------------------------------------------------------------------


def _discover_cases() -> list[str]:
    return sorted(p.stem for p in FIXTURE_DIR.glob("*.lst"))


@pytest.mark.parametrize("case", _discover_cases())
def test_fixture(case: str) -> None:
    lst_path = FIXTURE_DIR / f"{case}.lst"
    json_path = FIXTURE_DIR / f"{case}.json"
    assert json_path.exists(), f"Missing fixture: {json_path}"

    expected = sorted(line.strip() for line in lst_path.read_text().splitlines() if line.strip())
    diagnostics = validate_workflow(json_path, rules=get_rules())
    # Filter to only ND0401 since other rules may fire on the fixture
    actual = sorted(d.code.code for d in diagnostics if d.code.code == "ND0401")
    assert actual == expected, f"{case}: expected {expected}, got {actual}"


def test_broken1_location() -> None:
    """Verify broken1.json reports ND0401 on additionalFields/linkedinUrl."""
    json_path = FIXTURE_DIR / "broken1.json"
    diagnostics = validate_workflow(json_path, rules=get_rules(select={"ND0401"}))
    assert len(diagnostics) == 1
    d = diagnostics[0]
    assert d.code.code == "ND0401"
    assert d.location is not None
    assert "additionalFields/linkedinUrl" in d.location.field_path
