# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Unit tests for schema (V001-V005) and config (A004-A006) lint rules."""

import json
from pathlib import Path
from unittest.mock import patch

import pytest

from cxas_scrapi.utils.lint_rules.config import (
    AgentMissingInstruction,
    AppRootAgentValidation,
    AppVariableDefaultsAndEnvVars,
    RootAgentMissingEndSession,
)
from cxas_scrapi.utils.linter import LintContext, build_registry


@pytest.fixture
def context(tmp_path: Path) -> LintContext:
    return LintContext(
        project_root=tmp_path, app_dir=tmp_path, evals_dir=tmp_path
    )


def test_v001_variable_declaration_without_description_passes(
    tmp_path: Path, context: LintContext
) -> None:
    registry = build_registry()
    rule = registry.get("V001")

    app_dir = tmp_path / "myapp"
    app_dir.mkdir()
    (app_dir / "app.json").write_text(
        json.dumps(
            {
                "displayName": "MyApp",
                "variableDeclarations": [
                    {
                        "name": "customer_id",
                        "schema": {"type": "STRING"},
                    }
                ],
            }
        )
    )

    with patch("cxas_scrapi.utils.lint_rules.schema.json_format.ParseDict"):
        results = rule.check(app_dir, "", context)
        assert results == []


def test_v002_description_ending_with_extension_not_treated_as_file(
    tmp_path: Path, context: LintContext
) -> None:
    registry = build_registry()
    rule = registry.get("V002")

    agent_dir = tmp_path / "agents" / "gr_handler"
    agent_dir.mkdir(parents=True)
    (agent_dir / "instruction.txt").write_text("Follow guardrails.")
    (agent_dir / "gr_handler.json").write_text(
        json.dumps(
            {
                "displayName": "gr_handler",
                "description": (
                    "Guardrail recorder — generated by tools/gen_guardrails.py"
                ),
                "instruction": "agents/gr_handler/instruction.txt",
            }
        )
    )

    with patch("cxas_scrapi.utils.lint_rules.schema.json_format.ParseDict"):
        results = rule.check(agent_dir, "", context)
        assert results == []


def test_v003_empty_camelcase_submessage_passes_required_field_validation(
    tmp_path: Path, context: LintContext
) -> None:
    registry = build_registry()
    rule = registry.get("V003")

    tool_dir = tmp_path / "tools" / "lookup_order"
    tool_dir.mkdir(parents=True)
    (tool_dir / "lookup_order.json").write_text(
        json.dumps(
            {
                "displayName": "lookup_order",
                "toolFakeConfig": {"codeBlock": {}},
            }
        )
    )

    with patch("cxas_scrapi.utils.lint_rules.schema.json_format.ParseDict"):
        results = rule.check(tool_dir, "", context)
        assert results == []


def test_v005_empty_generative_answer_submessage_passes(
    tmp_path: Path, context: LintContext
) -> None:
    registry = build_registry()
    rule = registry.get("V005")

    guardrail_dir = tmp_path / "guardrails" / "safety_guard"
    guardrail_dir.mkdir(parents=True)
    (guardrail_dir / "safety_guard.json").write_text(
        json.dumps(
            {
                "displayName": "safety_guard",
                "action": {"generativeAnswer": {}},
            }
        )
    )

    with patch("cxas_scrapi.utils.lint_rules.schema.json_format.ParseDict"):
        results = rule.check(guardrail_dir, "", context)
        assert results == []


def test_a004_skips_guided_and_remote_dialogflow_agents(
    tmp_path: Path, context: LintContext
) -> None:
    rule = AgentMissingInstruction()

    for key in ("guidedAgent", "remoteDialogflowAgent"):
        agent_dir = tmp_path / "agents" / f"agent_{key}"
        agent_dir.mkdir(parents=True, exist_ok=True)
        agent_json = agent_dir / f"agent_{key}.json"
        agent_json.write_text(
            json.dumps({"displayName": f"agent_{key}", key: {}})
        )
        results = rule.check(agent_json, agent_json.read_text(), context)
        assert results == []


def test_a005_and_a006_resolve_root_agent_display_name_with_spaces(
    tmp_path: Path, context: LintContext
) -> None:
    app_json = tmp_path / "app.json"
    app_json.write_text(json.dumps({"rootAgent": "Root agent"}))

    agent_dir = tmp_path / "agents" / "Root_agent"
    agent_dir.mkdir(parents=True)
    (agent_dir / "Root_agent.json").write_text(
        json.dumps({"displayName": "Root agent", "tools": ["end_session"]})
    )

    a006 = AppRootAgentValidation()
    assert a006.check(app_json, app_json.read_text(), context) == []

    a005 = RootAgentMissingEndSession()
    assert a005.check(app_json, app_json.read_text(), context) == []


def test_a005_flags_missing_end_session_when_resolved_via_display_name(
    tmp_path: Path, context: LintContext
) -> None:
    app_json = tmp_path / "app.json"
    app_json.write_text(json.dumps({"rootAgent": "Research Assistant"}))

    agent_dir = tmp_path / "agents" / "research_root"
    agent_dir.mkdir(parents=True)
    (agent_dir / "research_root.json").write_text(
        json.dumps({"displayName": "Research Assistant", "tools": []})
    )

    a006 = AppRootAgentValidation()
    assert a006.check(app_json, app_json.read_text(), context) == []

    a005 = RootAgentMissingEndSession()
    results = a005.check(app_json, app_json.read_text(), context)
    assert len(results) == 1
    assert "missing 'end_session'" in results[0].message


def test_v001_gcs_uri_in_variable_declarations_not_resolved_as_file(
    tmp_path: Path, context: LintContext
) -> None:
    registry = build_registry()
    rule = registry.get("V001")

    app_dir = tmp_path / "myapp"
    app_dir.mkdir()
    (app_dir / "app.json").write_text(
        json.dumps(
            {
                "displayName": "MyApp",
                "variableDeclarations": [
                    {
                        "name": "feature_flags_path",
                        "schema": {
                            "type": "STRING",
                            "default": "gs://bucket/flags.json",
                        },
                    }
                ],
            }
        )
    )

    results = rule.check(app_dir, "", context)
    assert results == []


def test_v005_llm_policy_without_policy_scope_passes(
    tmp_path: Path, context: LintContext
) -> None:
    registry = build_registry()
    rule = registry.get("V005")

    guardrail_dir = tmp_path / "guardrails" / "Default_Prompt_Guardrail"
    guardrail_dir.mkdir(parents=True)
    (guardrail_dir / "Default_Prompt_Guardrail.json").write_text(
        json.dumps(
            {
                "displayName": "Default Prompt Guardrail",
                "llmPromptSecurity": {
                    "customPolicy": {
                        "prompt": "Ensure input is safe.",
                    }
                },
            }
        )
    )

    results = rule.check(guardrail_dir, "", context)
    assert results == []


def test_v006_scenario_with_evaluation_expectations_passes(
    tmp_path: Path, context: LintContext
) -> None:
    registry = build_registry()
    rule = registry.get("V006")

    eval_dir = tmp_path / "evaluations" / "scenario_eval"
    eval_dir.mkdir(parents=True)
    (eval_dir / "scenario_eval.json").write_text(
        json.dumps(
            {
                "displayName": "Scenario Eval",
                "scenario": {
                    "task": "Verify user identity and check balance.",
                    "evaluationExpectations": ["exp-1"],
                },
            }
        )
    )

    results = rule.check(eval_dir, "", context)
    assert results == []


def test_v001_and_a017_accept_env_var_placeholder_without_env_file(
    tmp_path: Path, context: LintContext
) -> None:
    registry = build_registry()
    v001 = registry.get("V001")
    a017 = AppVariableDefaultsAndEnvVars()

    app_dir = tmp_path / "myapp"
    app_dir.mkdir()
    app_json = app_dir / "app.json"
    app_json.write_text(
        json.dumps(
            {
                "displayName": "MyApp",
                "variableDeclarations": [
                    {
                        "name": "enable_authentication",
                        "schema": {"type": "BOOLEAN", "default": "$env_var"},
                    }
                ],
            }
        )
    )

    assert v001.check(app_dir, "", context) == []
    assert a017.check(app_json, app_json.read_text(), context) == []


def test_a017_flags_static_type_mismatch_in_app_json(
    tmp_path: Path, context: LintContext
) -> None:
    a017 = AppVariableDefaultsAndEnvVars()
    app_json = tmp_path / "app.json"
    app_json.write_text(
        json.dumps(
            {
                "displayName": "MyApp",
                "variableDeclarations": [
                    {
                        "name": "enable_authentication",
                        "schema": {"type": "BOOLEAN", "default": "true"},
                    }
                ],
            }
        )
    )

    results = a017.check(app_json, app_json.read_text(), context)
    assert len(results) == 1
    assert results[0].rule_id == "A017"
    assert "expected BOOLEAN, got string" in results[0].message


def test_a017_validates_against_environment_json_and_explicit_env_file(
    tmp_path: Path,
) -> None:
    a017 = AppVariableDefaultsAndEnvVars()
    app_dir = tmp_path / "app"
    app_dir.mkdir()
    app_json = app_dir / "app.json"
    app_json.write_text(
        json.dumps(
            {
                "displayName": "MyApp",
                "variableDeclarations": [
                    {
                        "name": "enable_authentication",
                        "schema": {"type": "BOOLEAN", "default": "$env_var"},
                    },
                    {
                        "name": "max_attempts",
                        "schema": {"type": "INTEGER", "default": "$env_var"},
                    },
                ],
            }
        )
    )

    # Top-level variableDeclarations trap + missing max_attempts
    (app_dir / "environment.json").write_text(
        json.dumps(
            {
                "variableDeclarations": [
                    {
                        "name": "enable_authentication",
                        "schema": {"default": True},
                    }
                ]
            }
        )
    )
    ctx_default = LintContext(
        project_root=tmp_path, app_dir=app_dir, evals_dir=tmp_path
    )
    errs = a017.check(app_json, app_json.read_text(), ctx_default)
    messages = [r.message for r in errs]
    assert any("top-level 'variableDeclarations'" in m for m in messages)
    assert any(
        "Variable 'enable_authentication'" in m and "missing" in m
        for m in messages
    )

    # Valid external --env-file outside app_dir passes cleanly
    external_env = tmp_path / "secrets" / "env.uat.json"
    external_env.parent.mkdir()
    external_env.write_text(
        json.dumps(
            {
                "app": {
                    "variableDeclarations": [
                        {
                            "name": "enable_authentication",
                            "schema": {"type": "BOOLEAN", "default": True},
                        },
                        {
                            "name": "max_attempts",
                            "schema": {"default": 3},
                        },
                    ]
                }
            }
        )
    )
    ctx_ext = LintContext(
        project_root=tmp_path,
        app_dir=app_dir,
        evals_dir=tmp_path,
        env_file=external_env,
    )
    assert a017.check(app_json, app_json.read_text(), ctx_ext) == []


def test_a017_flags_undeclared_or_non_placeholder_override_in_env_file(
    tmp_path: Path,
) -> None:
    a017 = AppVariableDefaultsAndEnvVars()
    app_json = tmp_path / "app.json"
    app_json.write_text(
        json.dumps(
            {
                "displayName": "MyApp",
                "variableDeclarations": [
                    {
                        "name": "enable_authentication",
                        "schema": {"type": "BOOLEAN", "default": False},
                    }
                ],
            }
        )
    )
    (tmp_path / "environment.json").write_text(
        json.dumps(
            {
                "app": {
                    "variableDeclarations": [
                        {
                            "name": "enable_authentication",
                            "schema": {"default": True},
                        },
                        {
                            "name": "unknown_flag",
                            "schema": {"default": "x"},
                        },
                    ]
                }
            }
        )
    )
    ctx = LintContext(
        project_root=tmp_path, app_dir=tmp_path, evals_dir=tmp_path
    )
    results = a017.check(app_json, app_json.read_text(), ctx)
    messages = [r.message for r in results]
    assert any(
        "Variable 'enable_authentication'" in m and "not '$env_var'" in m
        for m in messages
    )
    assert any(
        "Variable 'unknown_flag'" in m and "not declared" in m for m in messages
    )
