from __future__ import annotations

import sys
import base64
import hashlib
import json
import os
import sqlite3
import time
from pathlib import Path
from typing import Any

import pytest


ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "plugins" / "1c"))

from parser.payload import (  # noqa: E402
    append_brace_text_child,
    compress_payload,
    decode_payload_lossless,
    encode_brace_tree,
    encode_payload_lossless,
    get_tree_path,
    patch_brace_text_path,
    parse_brace_text,
    serialize_brace_tree,
    set_tree_path,
    try_decode,
    try_decompress,
)
from parser.config_object import find_identity  # noqa: E402
from parser.config_semantic import decode_config_semantic  # noqa: E402
from parser.cas_payload import classify_payload, replace_stream_block, stream_header  # noqa: E402
from parser.bsl_validation import replace_routine_text, routine_blocks, text_sha1, validate_bsl_text  # noqa: E402
from parser.form_payload import command_handler_links, data_path_reference_from_node, decode_form_payload, dynamic_list_fields, dynamic_list_settings, embedded_table_addition_row, enrich_form_common_semantic, enrich_item_data_paths, enrich_item_reference_semantics, event_handlers, event_handlers_from_node, form_common_semantic, form_item_coverage_summary, section_record_semantic_properties  # noqa: E402
from parser.form_xml import decode_form_xml  # noqa: E402
from parser.scd_payload import compare_scd_semantics, inspect_scd_payload, plan_scd_scalar_patch  # noqa: E402
from parser.appearance_payload import inspect_appearance_payload  # noqa: E402
from parser.dbnames import parse_dbnames_bytes, parse_dbnames_version_bytes  # noqa: E402
sys.path.insert(0, str(ROOT / "plugins" / "1c" / "connector"))
import adapter_1c_server as adapter_server  # noqa: E402
from adapter_1c_server import call_method, filter_related_children_by_identity, limit_object_commands_result, module_text_response, payload_public_preview, payload_public_properties, payload_public_undecoded_evidence, public_form_profile, public_form_row, public_metadata_item, public_module_row, public_semantic_profile, search_modules, strip_storage_traces  # noqa: E402
from adapter_1c_server import validate_metadata_object_full_payload  # noqa: E402
sys.path.insert(0, str(ROOT / "plugins" / "1c" / "mcp"))
import adapter_1c_mcp as adapter_mcp  # noqa: E402
from adapter_1c_mcp import validate_metadata_object_full_sections  # noqa: E402
import scripts.check_1c_mcp_adapter_contract as mcp_adapter_contract  # noqa: E402
import scripts.smoke_1c_mcp_selector_chain as mcp_selector_chain_smoke  # noqa: E402
import scripts.export_1c_access_role_audit as access_role_audit_export_script  # noqa: E402
import scripts.compare_1c_access_role_audit as access_role_audit_compare_script  # noqa: E402
import scripts.audit_1c_access_role as access_role_audit_script  # noqa: E402
import scripts.compare_1c_form_sql_xml as form_sql_xml_compare_script  # noqa: E402


def test_template_bsl_usage_trace_resolves_static_area_parameters() -> None:
    trace = adapter_server._template_bsl_usage_trace(
        '''
Макет = УправлениеПечатью.МакетПечатнойФормы("Отчет.Тест.ПФ_MXL_Тест");
ОбластьИтог = Макет.ПолучитьОбласть("Итог");
ОбластьИтог.Параметры.Сумма = Данные.Сумма;
СреднийЧек = ?(Данные.Количество = 0, 0,
    Данные.Сумма / Данные.Количество);
ОбластьИтог.Параметры.СреднийЧек = СреднийЧек;
ОбластьИтог.Параметры.Заполнить(СтрокаДанных);
''',
        "ПФ_MXL_Тест",
    )

    assert trace["status"] == "confirmed"
    assert trace["template_retrievals"][0]["variable"] == "Макет"
    assert trace["external_report_calls"] == []
    assert trace["areas"] == [{
        "name": "Итог", "variable": "ОбластьИтог", "line": 3,
        "parameters": [
            {"name": "Сумма", "expression": "Данные.Сумма", "line": 4, "resolution": "static_assignment"},
            {"name": "СреднийЧек", "expression": "СреднийЧек", "line": 7, "resolution": "static_assignment",
             "expression_lineage": {"variable": "СреднийЧек", "expression": "?(Данные.Количество = 0, 0, Данные.Сумма / Данные.Количество)", "line": 5, "resolution": "local_static_assignment"}},
        ],
        "bulk_fill_sources": [{"expression": "СтрокаДанных", "line": 8, "resolution": "bulk_fill_unexpanded"}],
    }]


def test_template_bsl_usage_trace_does_not_claim_dynamic_area_as_resolved() -> None:
    trace = adapter_server._template_bsl_usage_trace(
        '''
Макет = ПолучитьМакет("ПФ_MXL_Тест");
Область = Макет.ПолучитьОбласть(ИмяОбласти);
''',
        "ПФ_MXL_Тест",
    )

    assert trace["status"] == "confirmed"
    assert trace["areas"] == []


def test_template_bsl_usage_trace_follows_format_wrapper_to_local_formula() -> None:
    trace = adapter_server._template_bsl_usage_trace(
        '''
Макет = ПолучитьМакет("ПФ_MXL_Тест");
Область = Макет.ПолучитьОбласть("Итог");
СреднийЧек = Сумма / Количество;
Область.Параметры.СреднийЧек = Формат(СреднийЧек, "ЧДЦ=2");
''',
        "ПФ_MXL_Тест",
    )

    lineage = trace["areas"][0]["parameters"][0]["expression_lineage"]
    assert lineage == {
        "wrapper": "Формат", "resolution": "wrapped_local_static_assignment",
        "input": {"variable": "СреднийЧек", "expression": "Сумма / Количество", "line": 4, "resolution": "local_static_assignment"},
    }


def test_template_bsl_usage_trace_follows_accumulated_structure_member() -> None:
    trace = adapter_server._template_bsl_usage_trace(
        '''
Макет = ПолучитьМакет("ПФ_MXL_Тест");
Область = Макет.ПолучитьОбласть("Итог");
Итоги.Цена = Итоги.Цена + СтрокаДанных.Цена * СтрокаДанных.Количество;
Область.Параметры.Цена = Итоги.Цена;
''',
        "ПФ_MXL_Тест",
    )

    lineage = trace["areas"][0]["parameters"][0]["expression_lineage"]
    assert lineage == {
        "member": "Итоги.Цена", "expression": "Итоги.Цена + СтрокаДанных.Цена * СтрокаДанных.Количество",
        "line": 4, "resolution": "member_static_assignment", "aggregation": "accumulation",
        "source_fields": [
            {"expression": "СтрокаДанных.Цена", "variable": "СтрокаДанных", "field": "Цена"},
            {"expression": "СтрокаДанных.Количество", "variable": "СтрокаДанных", "field": "Количество"},
        ],
    }


def test_template_bsl_usage_trace_exposes_external_report_call_as_context_only() -> None:
    trace = adapter_server._template_bsl_usage_trace(
        '''
Макет = ПолучитьМакет("ПФ_MXL_Тест");
Дерево = Отчеты.ИсточникДанных.ТаблицаДанныхПланирования(Схема, Настройки);
''', "ПФ_MXL_Тест",
    )

    assert trace["external_report_calls"] == [{
        "report": "Report.ИсточникДанных", "method": "ТаблицаДанныхПланирования", "line": 3,
        "resolution": "static_external_report_call",
    }]


def test_template_field_lineage_returns_compact_static_parameter_path(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "templates_bindings", lambda _payload: {
        "status": "ok", "object": {"ref": "Report.Тест"}, "bindings": [{
            "template": {"name": "ПФ_MXL_Тест"}, "code_usage": {"static_traces": [{
                "owner": "Report.Тест", "module": "Object module", "routine": "Печать",
                "read_selector": {"method": "code.read", "module_ordinal": 1},
                "areas": [{"name": "Итог", "variable": "ОбластьИтог", "line": 3, "parameters": [{
                    "name": "Цена", "expression": "Итоги.Цена", "line": 4, "resolution": "static_assignment",
                }]}],
            }]},
        }],
    })

    result = adapter_server.template_field_lineage({
        "base_id": "test", "ref": "Report.Тест", "template": "ПФ_MXL_Тест", "field": "Цена",
    })

    assert result["status"] == "confirmed"
    assert result["lineage"][0]["area"]["name"] == "Итог"
    assert result["lineage"][0]["parameter"]["expression"] == "Итоги.Цена"


def test_public_errors_hide_tracebacks_unless_debug_is_explicit(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.delenv("ONEC_ADAPTER_DEBUG_DIAGNOSTICS", raising=False)
    monkeypatch.delenv("ONEC_MCP_DEBUG_DIAGNOSTICS", raising=False)
    adapter_error = adapter_server.adapter_public_error(
        "test.method",
        "failed",
        {"message": "failed", "traceback": "private adapter path"},
    )
    mcp_error = adapter_mcp.public_error(
        "test.method",
        "failed",
        {"message": "failed", "traceback": "private MCP path"},
    )
    assert "traceback" not in adapter_error["diagnostics"]
    assert "traceback" not in mcp_error["diagnostics"]

    monkeypatch.setenv("ONEC_ADAPTER_DEBUG_DIAGNOSTICS", "true")
    monkeypatch.setenv("ONEC_MCP_DEBUG_DIAGNOSTICS", "true")
    assert adapter_server.adapter_public_error(
        "test.method", "failed", {"traceback": "adapter trace"}
    )["diagnostics"]["traceback"] == "adapter trace"
    assert adapter_mcp.public_error(
        "test.method", "failed", {"traceback": "MCP trace"}
    )["diagnostics"]["traceback"] == "MCP trace"


def test_dbnames_version_parser_accepts_utf8_bom_scalar_version() -> None:
    version = "E27A62F5-8C82-4132-BD16-281F9E47DEFC"

    parsed = parse_dbnames_version_bytes(f"{{0,{version}}}".encode("utf-8-sig"))

    assert parsed["marker"] == 0
    assert parsed["version"] == version.lower()
    assert parsed["encoding"] == "utf-8-sig"
    assert parsed["compression"] == "none"


def test_scd_payload_decoder_reads_xml_after_platform_prefix() -> None:
    xml = """<?xml version="1.0" encoding="UTF-8"?>
<SchemaFile><dataCompositionSchema>
  <dataSource><name>ИсточникДанных1</name><dataSourceType>Local</dataSourceType></dataSource>
  <dataSet xsi:type="DataSetQuery" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><name>ОсновнойНабор</name><query>ВЫБРАТЬ &amp;Период КАК Период // &amp;Скрытый КАК Скрытый</query></dataSet>
  <parameter><name>Период</name><valueType>xs:dateTime</valueType></parameter>
  <field><dataPath>Период</dataPath></field>
  <field><dataPath>Сумма</dataPath></field>
  <calculatedField><name>СуммаСНалогом</name><expression>Сумма * 1.2</expression></calculatedField>
  <resource><name>Сумма</name><expression>Сумма</expression></resource>
  <group>Период</group>
  <totalField>Сумма</totalField>
  <settingsVariant><name>Основной</name><presentation><item><lang>ru</lang><content>Основной вариант</content></item></presentation></settingsVariant>
</dataCompositionSchema></SchemaFile>"""
    payload = b"\x00\x00\x00\x00\x01\x00\x00\x00" + xml.encode("utf-8")

    result = inspect_scd_payload(compress_payload(payload, "raw_deflate"))

    assert result["status"] == "ok"
    assert result["container"]["compression"] == "raw_deflate"
    assert result["sections"]["data_sources"] == [{"name": "ИсточникДанных1", "source": {"kind": "scd_xml", "path": "/dataCompositionSchema[1]/dataSource"}, "data_source_type": "Local"}]
    assert result["sections"]["parameters"] == [{"name": "Период", "source": {"kind": "scd_xml", "path": "/dataCompositionSchema[1]/parameter"}, "value_type": "xs:dateTime"}]
    assert result["sections"]["datasets"][0]["query"] == "ВЫБРАТЬ &Период КАК Период // &Скрытый КАК Скрытый"
    assert result["sections"]["calculated_fields"][0]["expression"] == "Сумма * 1.2"
    assert result["sections"]["groupings"] == [{"name": "Период", "source": {"kind": "scd_xml", "path": "/dataCompositionSchema[1]/group"}}]
    assert result["sections"]["variants"][0]["name"] == "Основной"
    assert result["sections"]["variants"][0]["presentation"] == "ruОсновной вариант"
    assert result["analysis"]["query_parameter_references"] == [{"dataset": "ОсновнойНабор", "parameters": ["Период"]}]
    assert result["analysis"]["schema_outline"] == {
        "kind": "exact_xml_tag_inventory",
        "top_level": [
            {"tag": "calculatedField", "count": 1, "paths": ["/dataCompositionSchema[1]/calculatedField"]},
            {"tag": "dataSet", "count": 1, "paths": ["/dataCompositionSchema[1]/dataSet"]},
            {"tag": "dataSource", "count": 1, "paths": ["/dataCompositionSchema[1]/dataSource"]},
            {"tag": "field", "count": 2, "paths": ["/dataCompositionSchema[1]/field", "/dataCompositionSchema[1]/field[2]"]},
            {"tag": "group", "count": 1, "paths": ["/dataCompositionSchema[1]/group"]},
            {"tag": "parameter", "count": 1, "paths": ["/dataCompositionSchema[1]/parameter"]},
            {"tag": "resource", "count": 1, "paths": ["/dataCompositionSchema[1]/resource"]},
            {"tag": "settingsVariant", "count": 1, "paths": ["/dataCompositionSchema[1]/settingsVariant"]},
            {"tag": "totalField", "count": 1, "paths": ["/dataCompositionSchema[1]/totalField"]},
        ],
        "all_tag_counts": {"calculatedField": 1, "content": 1, "dataCompositionSchema": 1, "dataPath": 2, "dataSet": 1, "dataSource": 1, "dataSourceType": 1, "expression": 2, "field": 2, "group": 1, "item": 1, "lang": 1, "name": 6, "parameter": 1, "presentation": 1, "query": 1, "resource": 1, "settingsVariant": 1, "totalField": 1, "valueType": 1},
    }
    assert result["analysis"]["referenced_not_declared_in_schema"] == []
    assert result["analysis"]["total_field_references"] == {"fields": ["Сумма"], "missing_from_declared_fields": [], "status": "checked"}
    assert result["analysis"]["settings_context"] == {"status": "not_present", "sections": {}}


def test_appearance_payload_decoder_reads_xml_rules() -> None:
    xml = """<?xml version="1.0" encoding="UTF-8"?>
<AppearanceTemplate><item><parameter>Заголовок</parameter><value>style:Title</value></item><appearance><item><parameter>ЦветФона</parameter><value type="Line" ref="style:Blue"><style type="Solid">Solid</style></value></item></appearance><style><name>Title</name></style></AppearanceTemplate>"""

    result = inspect_appearance_payload(compress_payload(b"\x00\x01" + xml.encode("utf-8"), "raw_deflate"))

    assert result["status"] == "ok"
    assert result["items"] == [{"ordinal": 1, "parameter": "Заголовок", "value": "style:Title"}]
    assert result["appearances"] == [{"ordinal": 1, "rules": [{"parameter": "ЦветФона", "value": "Solid", "value_type": "Line", "value_attributes": {"type": "Line", "ref": "style:Blue"}, "value_children": [{"tag": "style", "value": "Solid", "attributes": {"type": "Solid"}}]}]}]
    assert result["outline"]["tag_counts"] == {"AppearanceTemplate": 1, "appearance": 1, "item": 2, "name": 1, "parameter": 2, "style": 2, "value": 2}
    assert result["diagnostics"]["write"] == "unsupported_until_reverse_codec_fixture"


def test_appearance_inspect_resolves_public_common_template(monkeypatch: pytest.MonkeyPatch) -> None:
    source = compress_payload('<?xml version="1.0"?><AppearanceTemplate><item><parameter>Заголовок</parameter><value>x</value></item></AppearanceTemplate>'.encode("utf-8"), "raw_deflate")
    monkeypatch.setattr(adapter_server, "get_object", lambda *_args, **_kwargs: {"status": "ok", "object": {"guid": "a" * 36, "kind": "CommonTemplate", "name": "Оформление"}})
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *_args, **_kwargs: (source, {}, None))

    result = adapter_server.appearance_inspect({"base_id": "upo_test", "template": "Оформление"})

    assert result["status"] == "ok"
    assert result["template"]["name"] == "Оформление"
    assert result["appearance"]["items"][0]["parameter"] == "Заголовок"
    assert result["template_type"]["status"] == "unresolved"


def test_report_inspect_keeps_forms_separate_from_scd(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "metadata_object_components", lambda _payload: {
        "status": "ok", "object": {"kind": "Report", "name": "Продажи", "guid": "r" * 36},
        "components": [
            {"component_kind": "form", "name": "ОсновнаяСхемаКомпоновкиДанных"},
            {"component_kind": "scd", "name": "ОсновнаяСхема", "guid": "s" * 36},
        ], "unresolved": [{"area": "templates", "status": "partial"}],
    })

    result = adapter_server.report_inspect({"base_id": "upo_test", "report": "Продажи"})

    assert result["status"] == "ok"
    assert [item["name"] for item in result["forms"]] == ["ОсновнаяСхемаКомпоновкиДанных"]
    assert [item["name"] for item in result["scd"]] == ["ОсновнаяСхема"]
    assert result["unresolved"][0]["area"] == "templates"


def test_rpc_dispatches_report_and_appearance_inspection(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "report_inspect", lambda payload: {"status": "ok", "method": "report.inspect", "report": payload["report"]})
    monkeypatch.setattr(adapter_server, "appearance_inspect", lambda payload: {"status": "ok", "method": "appearance.inspect", "template": payload["template"]})

    assert call_method("report.inspect", {"base_id": "upo_test", "report": "Продажи"})["report"] == "Продажи"
    assert call_method("appearance.inspect", {"base_id": "upo_test", "template": "Оформление"})["template"] == "Оформление"


def test_scd_scalar_patch_preserves_platform_prefix_and_trailer() -> None:
    xml = """<?xml version="1.0" encoding="UTF-8"?>
<SchemaFile><dataCompositionSchema>
<dataSet><name>ОсновнойНабор</name><query>ВЫБРАТЬ 1 КАК Значение</query></dataSet>
</dataCompositionSchema></SchemaFile>"""
    plain = b"\x00\x00\x00\x00\x01\x00\x00\x00" + xml.encode("utf-8") + b"\x11\x22\x33\x44"
    source = compress_payload(plain, "raw_deflate")

    plan = plan_scd_scalar_patch(
        source,
        section="datasets",
        name="ОсновнойНабор",
        property_name="query",
        value="ВЫБРАТЬ &Период КАК Период",
    )

    assert plan["status"] == "planned"
    patched = decode_payload_lossless(plan["payload"])["payload"]
    assert patched.startswith(b"\x00\x00\x00\x00\x01\x00\x00\x00")
    assert patched.endswith(b"\x11\x22\x33\x44")
    decoded = inspect_scd_payload(plan["payload"])
    assert decoded["sections"]["datasets"][0]["query"] == "ВЫБРАТЬ &Период КАК Период"
    assert decoded["analysis"]["query_parameter_references"] == [{"dataset": "ОсновнойНабор", "parameters": ["Период"]}]


def test_scd_patch_apply_and_rollback_uses_apply_backup(monkeypatch: pytest.MonkeyPatch) -> None:
    """A SCD probe must restore raw storage bytes, not re-encode old XML text."""
    source = b"original"
    replacement = b"replacement"
    monkeypatch.setattr(adapter_server, "scd_inspect", lambda _payload: {
        "status": "ok",
        "scd": {"guid": "test-guid", "name": "ОсновнаяСхема", "payload_route": {"file_name": "test-guid.0"}},
    })
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *_args, **_kwargs: (source, {}, None))
    from parser import scd_payload
    monkeypatch.setattr(scd_payload, "plan_scd_scalar_patch", lambda *_args, **_kwargs: {
        "status": "planned", "payload": replacement, "expected_sha1": "a" * 40,
        "result_sha1": "b" * 40, "old": "ВЫБРАТЬ 1", "new": "ВЫБРАТЬ 2",
    })
    monkeypatch.setattr(scd_payload, "inspect_scd_payload", lambda _data: {
        "sections": {"datasets": [{"name": "Набор", "query": "ВЫБРАТЬ 2"}]}, "analysis": {},
    })
    monkeypatch.setattr(adapter_server, "storage_saved_state_apply_proposal", lambda _payload: {
        "schema": "onec_storage_saved_state_apply.v1", "status": "applied", "applied": True,
        "backup": {"backup_id": "c" * 32},
    })
    rollback_calls: list[dict[str, object]] = []
    monkeypatch.setattr(adapter_server, "storage_saved_state_rollback", lambda payload: rollback_calls.append(payload) or {"status": "applied", "applied": True})

    result = adapter_server.scd_patch({
        "base_id": "upo_test", "report": "Тест", "section": "datasets", "item_name": "Набор",
        "property": "query", "value": "ВЫБРАТЬ 2", "allow_saved_state_write": True,
        "execution_mode": "apply_and_rollback", "allow_sql_saved_state_apply": True,
        "allow_sql_saved_state_rollback": True,
    })

    assert result["status"] == "verified_and_rolled_back"
    assert result["rolled_back"] is True
    assert rollback_calls == [{
        "base_id": "upo_test", "allow_sql_saved_state_rollback": True,
        "backup_id": "c" * 32, "timeout_seconds": 60,
    }]


def test_scd_patch_rollback_gate_precedes_sql_apply(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "scd_inspect", lambda _payload: {
        "status": "ok",
        "scd": {"guid": "test-guid", "name": "ОсновнаяСхема", "payload_route": {"file_name": "test-guid.0"}},
    })
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *_args, **_kwargs: (b"original", {}, None))
    from parser import scd_payload
    monkeypatch.setattr(scd_payload, "plan_scd_scalar_patch", lambda *_args, **_kwargs: {
        "status": "planned", "payload": b"replacement", "expected_sha1": "a" * 40,
        "result_sha1": "b" * 40, "old": "ВЫБРАТЬ 1", "new": "ВЫБРАТЬ 2",
    })
    apply_calls: list[dict[str, object]] = []
    monkeypatch.setattr(adapter_server, "storage_saved_state_apply_proposal", lambda payload: apply_calls.append(payload) or {})

    result = adapter_server.scd_patch({
        "base_id": "upo_test", "report": "Тест", "section": "datasets", "item_name": "Набор",
        "property": "query", "value": "ВЫБРАТЬ 2", "allow_saved_state_write": True,
        "execution_mode": "apply_and_rollback", "allow_sql_saved_state_apply": True,
    })

    assert result["status"] == "invalid_argument"
    assert "allow_sql_saved_state_rollback" in str(result)
    assert apply_calls == []


def test_scd_patch_auto_prepares_missing_saved_payload(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "scd_inspect", lambda _payload: {
        "status": "ok",
        "scd": {"guid": "test-guid", "name": "ОсновнаяСхема", "payload_route": {"file_name": "test-guid.0"}},
    })
    reads = iter([(None, {}, {"status": "not_found"}), (b"saved", {}, None), (b"saved", {}, None)])
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *_args, **_kwargs: next(reads))
    monkeypatch.setattr(adapter_server, "scd_prepare", lambda _payload: {
        "status": "applied", "prepare_receipt_id": "d" * 32,
    })
    from parser import scd_payload
    monkeypatch.setattr(scd_payload, "plan_scd_scalar_patch", lambda *_args, **_kwargs: {
        "status": "planned", "payload": b"replacement", "expected_sha1": "a" * 40,
        "result_sha1": "b" * 40, "old": "ВЫБРАТЬ 1", "new": "ВЫБРАТЬ 2",
    })
    monkeypatch.setattr(scd_payload, "inspect_scd_payload", lambda _data: {
        "sections": {"datasets": [{"name": "Набор", "query": "ВЫБРАТЬ 2"}]}, "analysis": {},
    })
    monkeypatch.setattr(adapter_server, "storage_saved_state_apply_proposal", lambda _payload: {
        "status": "applied", "applied": True, "backup": {"backup_id": "c" * 32},
    })

    result = adapter_server.scd_patch({
        "base_id": "upo_test", "report": "Тест", "section": "datasets", "item_name": "Набор",
        "property": "query", "value": "ВЫБРАТЬ 2", "allow_saved_state_write": True,
        "execution_mode": "apply_and_verify", "allow_sql_saved_state_apply": True,
        "allow_sql_saved_state_prepare": True,
    })

    assert result["status"] == "verified"
    assert result["prepare_receipt_id"] == "d" * 32
    assert result["prepare_result"]["status"] == "applied"


def test_scd_patch_auto_prepare_rolls_back_patch_and_overlay(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "scd_inspect", lambda _payload: {
        "status": "ok",
        "scd": {"guid": "test-guid", "name": "ОсновнаяСхема", "payload_route": {"file_name": "test-guid.0"}},
    })
    reads = iter([(None, {}, {"status": "not_found"}), (b"saved", {}, None), (b"saved", {}, None)])
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *_args, **_kwargs: next(reads))
    monkeypatch.setattr(adapter_server, "scd_prepare", lambda _payload: {
        "status": "applied", "prepare_receipt_id": "d" * 32,
    })
    from parser import scd_payload
    monkeypatch.setattr(scd_payload, "plan_scd_scalar_patch", lambda *_args, **_kwargs: {
        "status": "planned", "payload": b"replacement", "expected_sha1": "a" * 40,
        "result_sha1": "b" * 40, "old": "ВЫБРАТЬ 1", "new": "ВЫБРАТЬ 2",
    })
    monkeypatch.setattr(scd_payload, "inspect_scd_payload", lambda _data: {
        "sections": {"datasets": [{"name": "Набор", "query": "ВЫБРАТЬ 2"}]}, "analysis": {},
    })
    monkeypatch.setattr(adapter_server, "storage_saved_state_apply_proposal", lambda _payload: {
        "status": "applied", "applied": True, "backup": {"backup_id": "c" * 32},
    })
    monkeypatch.setattr(adapter_server, "storage_saved_state_rollback", lambda _payload: {"status": "applied", "applied": True})
    cleanup_calls: list[str] = []
    monkeypatch.setattr(adapter_server, "rollback_saved_state_prepare_receipt", lambda _base, receipt, **_kwargs: cleanup_calls.append(receipt) or {"status": "rolled_back", "applied": True})

    result = adapter_server.scd_patch({
        "base_id": "upo_test", "report": "Тест", "section": "datasets", "item_name": "Набор",
        "property": "query", "value": "ВЫБРАТЬ 2", "allow_saved_state_write": True,
        "execution_mode": "apply_and_rollback", "allow_sql_saved_state_apply": True,
        "allow_sql_saved_state_prepare": True, "allow_sql_saved_state_rollback": True,
    })

    assert result["status"] == "verified_and_rolled_back"
    assert result["prepare_cleanup"]["status"] == "rolled_back"
    assert cleanup_calls == ["d" * 32]


def test_scd_semantic_compare_uses_names_and_ignores_storage_evidence() -> None:
    active = {"sections": {"datasets": [{"name": "Набор", "query": "ВЫБРАТЬ 1", "source": {"path": "/old"}}], "parameters": [{"name": "Период"}]}}
    saved = {"sections": {"datasets": [{"name": "Набор", "query": "ВЫБРАТЬ 2", "source": {"path": "/new"}}], "parameters": [{"name": "Период"}, {"name": "Организация"}]}}

    result = compare_scd_semantics(active, saved)

    assert result["status"] == "changed"
    assert result["sections"]["datasets"]["changed"] == ["Набор"]
    assert result["sections"]["parameters"]["added"] == ["Организация"]
    assert result["counts"] == {"added": 1, "removed": 0, "changed": 1, "unchanged": 1}


def test_dcs_field_lineage_resolves_alias_and_virtual_register_source(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "scd_inspect", lambda payload: {
        "status": "ok", "base_id": "upo_test", "report": "Тест",
        "sections": {"datasets": [{"name": "Набор", "query": """
ВЫБРАТЬ
    Цены.Цена КАК ЦенаПродажи
ИЗ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Дата) КАК Цены
ГДЕ Цены.ВидЦен = &ВидЦен
"""}], "fields": [], "calculated_fields": [], "resources": []},
    })

    result = adapter_server.dcs_field_lineage({"base_id": "upo_test", "report": "Тест", "field": "ЦенаПродажи"})
    reference = result["lineage"][0]["direct_references"][0]

    assert reference["public_path"] == "РегистрСведений.ЦеныНоменклатуры.СрезПоследних.Цена"
    assert reference["virtual_source"] == {
        "register": "РегистрСведений.ЦеныНоменклатуры",
        "virtual_table": "СрезПоследних",
        "status": "static_source_confirmed",
        "arguments": "&Дата",
        "filter": "Цены.ВидЦен = &ВидЦен",
    }
    assert result["agent_summary"]["status"] == "confirmed"
    assert result["agent_summary"]["confirmed_sources"][0]["source"] == reference["public_path"]


def test_scd_inspect_reports_unproven_name_instead_of_claiming_template_match(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_related",
        lambda payload: {
            "status": "ok",
            "related": [{"category": "DataCompositionSchema", "status": "ok", "name": None, "guid": "11111111-1111-1111-1111-111111111111"}],
        },
    )

    result = adapter_server.scd_inspect(
        {"base_id": "upo_test", "report": "Продажи", "schema": "ОсновнаяСхемаКомпоновкиДанных"}
    )

    assert result["status"] == "partial"
    assert result["error"] == "scd_schema_name_not_proven"
    assert result["requested_schema"] == "ОсновнаяСхемаКомпоновкиДанных"
    assert result["diagnostics"]["next_call"]["params"] == {"base_id": "upo_test", "report": "Продажи"}


def test_dcs_field_lineage_follows_simple_temporary_table_projection(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "scd_inspect", lambda payload: {
        "status": "ok", "base_id": "upo_test", "report": "Тест",
        "sections": {"datasets": [{"name": "Набор", "query": """
ВЫБРАТЬ
    Цены.Цена КАК ЦенаПродажи
ПОМЕСТИТЬ втЦены
ИЗ РегистрСведений.ЦеныНоменклатуры КАК Цены;

ВЫБРАТЬ
    втЦены.ЦенаПродажи КАК ЦенаПродажи
ИЗ втЦены КАК втЦены
"""}], "fields": [], "calculated_fields": [], "resources": []},
    })

    result = adapter_server.dcs_field_lineage({"base_id": "upo_test", "report": "Тест", "field": "ЦенаПродажи"})
    references = [reference for item in result["lineage"] for reference in item.get("direct_references", [])]
    temporary_reference = next(reference for reference in references if reference.get("resolution") == "temporary_table_projection")

    assert temporary_reference["public_path"] == "РегистрСведений.ЦеныНоменклатуры.Цена"


def test_dcs_field_lineage_follows_temporary_table_expression(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "scd_inspect", lambda payload: {
        "status": "ok", "base_id": "upo_test", "report": "Тест",
        "sections": {"datasets": [{"name": "Набор", "query": """
ВЫБРАТЬ
    ЕСТЬNULL(Цены.Цена, 0) КАК ЦенаПродажи
ПОМЕСТИТЬ втЦены
ИЗ РегистрСведений.ЦеныНоменклатуры КАК Цены;

ВЫБРАТЬ
    втЦены.ЦенаПродажи КАК ЦенаПродажи
ИЗ втЦены КАК втЦены
"""}], "fields": [], "calculated_fields": [], "resources": []},
    })

    result = adapter_server.dcs_field_lineage({"base_id": "upo_test", "report": "Тест", "field": "ЦенаПродажи"})
    sources = result["agent_summary"]["confirmed_sources"]
    assert any(item["source"] == "РегистрСведений.ЦеныНоменклатуры.Цена" for item in sources)
    assert not result["agent_summary"]["partial_steps"]
    assert any(item.get("resolution") == "temporary_table_expression" for item in result["agent_summary"]["confirmed_sources"])


def test_dcs_field_lineage_reports_calculated_field_dependencies(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "scd_inspect", lambda payload: {
        "status": "ok", "base_id": "upo_test", "report": "Тест",
        "sections": {
            "datasets": [], "fields": [{"data_path": "Сумма"}, {"data_path": "Ставка"}],
            "calculated_fields": [{"name": "СуммаСНалогом", "expression": "Сумма * (1 + Ставка)"}], "resources": [],
        },
    })

    result = adapter_server.dcs_field_lineage({"base_id": "upo_test", "report": "Тест", "field": "СуммаСНалогом"})

    assert result["lineage"][0]["field_dependencies"] == ["Ставка", "Сумма"]
    assert result["agent_summary"]["partial_steps"][0]["reason"] == "calculated_expression_inputs_not_resolved"


def test_dcs_field_lineage_marks_union_branches(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "scd_inspect", lambda payload: {
        "status": "ok", "base_id": "upo_test", "report": "Тест",
        "sections": {"datasets": [{"name": "Набор", "query": """
ВЫБРАТЬ А.Цена КАК Цена
ИЗ РегистрСведений.ЦеныОпт КАК А
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ Б.Цена КАК Цена
ИЗ РегистрСведений.ЦеныРозница КАК Б
"""}], "fields": [], "calculated_fields": [], "resources": []},
    })

    result = adapter_server.dcs_field_lineage({"base_id": "upo_test", "report": "Тест", "field": "Цена"})

    assert {item["branch"] for item in result["lineage"] if item.get("branch")} == {1, 2}
    assert {item["branches"][0] for item in result["agent_summary"]["confirmed_sources"]} == {1, 2}


def test_dcs_field_lineage_exposes_join_condition_for_source(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "scd_inspect", lambda payload: {
        "status": "ok", "base_id": "upo_test", "report": "Тест",
        "sections": {"datasets": [{"name": "Набор", "query": """
ВЫБРАТЬ Цены.Цена КАК Цена
ИЗ Справочник.Номенклатура КАК Номенклатура
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры КАК Цены
ПО Цены.Номенклатура = Номенклатура.Ссылка
"""}], "fields": [], "calculated_fields": [], "resources": []},
    })

    result = adapter_server.dcs_field_lineage({"base_id": "upo_test", "report": "Тест", "field": "Цена"})

    assert result["agent_summary"]["confirmed_sources"][0]["joins"] == [{"kind": "левое", "condition": "Цены.Номенклатура = Номенклатура.Ссылка"}]


def test_scd_agent_report_embeds_requested_field_lineage(monkeypatch: pytest.MonkeyPatch) -> None:
    inspected = {
        "status": "ok", "base_id": "upo_test", "report": "Тест", "extension": None,
        "scd": {"name": "ОсновнаяСхемаКомпоновкиДанных"},
        "sections": {"datasets": [], "fields": [], "calculated_fields": [], "resources": [], "parameters": []},
        "analysis": {"data_source_resolution": {"sources": []}, "direct_field_resolution": {"references": []}, "query_parameter_references": [], "referenced_not_declared_in_schema": [], "total_field_references": {}},
    }
    monkeypatch.setattr(adapter_server, "scd_inspect", lambda payload: inspected)
    monkeypatch.setattr(adapter_server, "dcs_field_lineage", lambda payload: {"status": "ok", "field": payload["field"], "agent_summary": {"status": "confirmed"}})

    result = adapter_server.scd_agent_report({"base_id": "upo_test", "report": "Тест", "field": "Цена"})

    assert result["field_lineage"]["field"] == "Цена"


@pytest.mark.parametrize(
    ("payload", "message"),
    [
        (b"", "expected root list"),
        (b"{0,{version}}", "expected scalar marker and version"),
        (b"{0,not-a-guid}", "version must be a GUID string"),
    ],
)
def test_dbnames_version_parser_rejects_invalid_shape(payload: bytes, message: str) -> None:
    with pytest.raises(ValueError, match=message):
        parse_dbnames_version_bytes(payload)


def test_dbnames_records_parser_keeps_existing_contract() -> None:
    parsed = parse_dbnames_bytes(b'{12700,{1,{11111111-1111-1111-1111-111111111111,"Role",7}}}')

    assert parsed["declared_count"] == 1
    assert len(parsed["records"]) == 1
    assert parsed["records"][0].storage_role == "Role"


def test_dbnames_sequence_role_maps_to_public_sequence_kind() -> None:
    parsed = parse_dbnames_bytes(b'{12700,{1,{11111111-1111-1111-1111-111111111111,"Sequence",7}}}')
    record = parsed["records"][0]

    assert adapter_server.DBNAMES_ROLE_KIND[record.storage_role] == "Sequence"
    assert adapter_server.PUBLIC_KIND["Sequence"] == "sequence"
    assert adapter_server.ONEC_CODE_ROOT_BY_KIND["Sequence"] == "Последовательности"


def test_dbnames_summary_routes_version_to_separate_parser(monkeypatch: pytest.MonkeyPatch) -> None:
    payloads = {
        "DBNames": b'{12700,{1,{11111111-1111-1111-1111-111111111111,"Role",7}}}',
        "DBNamesVersion": b"\xef\xbb\xbf{0,e27a62f5-8c82-4132-bd16-281f9e47defc}",
    }
    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda _payload: {"status": "ok", "files": [{"FileName": name} for name in payloads]},
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda _base_id, _table, file_name, **_kwargs: (payloads[file_name], {}, None),
    )

    result = adapter_server.metadata_dbnames_summary({"base_id": "upo_test", "diagnostic": True})

    assert result["status"] == "ok"
    assert result["counts"] == {"files": 2, "records": 1}
    summaries = {item["file_name"]: item for item in result["dbnames"]}
    assert summaries["DBNames"]["records"] == 1
    assert summaries["DBNamesVersion"]["marker"] == 0
    assert summaries["DBNamesVersion"]["version"] == "e27a62f5-8c82-4132-bd16-281f9e47defc"


def test_live_dbnames_records_ignores_dbnames_version(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: list[str] = []
    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda _payload: {"status": "ok", "files": [{"FileName": "DBNames"}, {"FileName": "DBNamesVersion"}]},
    )

    def read_file(_base_id: str, _table: str, file_name: str, **_kwargs: Any) -> tuple[bytes, dict[str, Any], None]:
        seen.append(file_name)
        return b'{12700,{1,{11111111-1111-1111-1111-111111111111,"Role",7}}}', {}, None

    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", read_file)

    records, error = adapter_server.live_dbnames_records("upo_test")

    assert error is None
    assert len(records or []) == 1
    assert seen == ["DBNames"]


@pytest.mark.parametrize("extension_guid", [None, ""])
def test_mcp_code_search_omits_empty_extension_guid(monkeypatch: pytest.MonkeyPatch, extension_guid: Any) -> None:
    captured: dict[str, Any] = {}

    def call_adapter(method: str, payload: dict[str, Any], **_kwargs: Any) -> dict[str, Any]:
        captured.update(payload)
        return {"schema": "onec_code_search.v1", "method": method, "status": "ok", "items": []}

    monkeypatch.setattr(adapter_mcp, "call_adapter_method", call_adapter)

    result = adapter_mcp.run_or_enqueue_adapter_method(
        "code.search",
        {"base_id": "upo_test", "query": "Найти", "extension_guid": extension_guid, "_mcp_sync": True},
    )

    assert result["status"] == "ok"
    assert "extension_guid" not in captured


def test_mcp_code_search_preserves_valid_extension_guid(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "e27a62f5-8c82-4132-bd16-281f9e47defc"
    captured: dict[str, Any] = {}

    def call_adapter(method: str, payload: dict[str, Any], **_kwargs: Any) -> dict[str, Any]:
        captured.update(payload)
        return {"schema": "onec_code_search.v1", "method": method, "status": "ok", "items": []}

    monkeypatch.setattr(adapter_mcp, "call_adapter_method", call_adapter)

    adapter_mcp.run_or_enqueue_adapter_method(
        "code.search",
        {"base_id": "upo_test", "query": "Найти", "extension_guid": extension_guid, "_mcp_sync": True},
    )

    assert captured["extension_guid"] == extension_guid


def test_code_search_internal_modules_request_omits_empty_extension_selectors(monkeypatch: pytest.MonkeyPatch) -> None:
    captured: dict[str, Any] = {}
    monkeypatch.setattr(adapter_server, "code_saved_state_common_form_module_search", lambda *_args, **_kwargs: None)

    def search(payload: dict[str, Any]) -> dict[str, Any]:
        captured.update(payload)
        return {"status": "ok", "matches": [], "counts": {"matches": 0}}

    monkeypatch.setattr(adapter_server, "search_modules", search)

    result = adapter_server.code_search({"base_id": "upo_test", "query": "Найти", "state": "active"})

    assert result["status"] == "not_found"
    assert "extension" not in captured
    assert "extension_guid" not in captured


def test_object_full_uses_fast_form_listing_by_default() -> None:
    assert adapter_server.adapter_full_forms_method({}) == "metadata.object.forms"
    assert adapter_server.adapter_full_forms_method({"include_form_details": False}) == "metadata.object.forms"


def test_object_full_uses_form_details_only_when_requested() -> None:
    assert adapter_server.adapter_full_forms_method({"include_form_details": True}) == "metadata.object.form.details"
    assert adapter_server.adapter_full_forms_method({"include_form_module_text": True}) == "metadata.object.form.details"


def test_common_form_kind_is_first_class_selector() -> None:
    assert adapter_server.canonical_kind("Общая форма") == "CommonForm"
    assert adapter_server.canonical_kind("ОбщиеФормы") == "CommonForm"
    assert adapter_server.canonical_kind("CommonForm") == "CommonForm"
    assert adapter_server.PUBLIC_KIND["CommonForm"] == "common_form"
    assert adapter_server.RU_KIND["CommonForm"] == "ОбщаяФорма"
    assert "form_details" in adapter_server.KIND_CAPABILITIES["CommonForm"]


def test_every_public_1c_kind_name_and_code_root_resolves_to_internal_kind() -> None:
    for kind, kind_ru in adapter_server.RU_KIND.items():
        assert adapter_server.known_canonical_kind(kind) == kind
        assert adapter_server.known_canonical_kind(kind_ru) == kind
    for code_root, kind in adapter_server.ONEC_CODE_ROOT_KIND.items():
        assert adapter_server.known_canonical_kind(code_root) == kind


def test_runtime_method_registry_is_unique_and_covers_contract_metadata() -> None:
    registry = adapter_server.adapter_method_registry()
    diagnostics = adapter_server.adapter_method_registry_diagnostics()
    response = adapter_server.call_method("help.methods", {})

    assert diagnostics["status"] == "ok"
    assert diagnostics["duplicates"] == []
    assert diagnostics["input_schemas_without_method"] == []
    assert diagnostics["selector_contracts_without_method"] == []
    assert diagnostics["http_routes_without_method"] == []
    assert diagnostics["counts"]["specialized_http_routes"] == 80
    assert diagnostics["counts"]["methods_with_input_schema"] == 185
    assert len(registry) == len(adapter_server.METHODS) == 185
    assert all(row["input_schema"]["type"] == "object" for row in registry.values())
    assert registry["code.search"]["http_routes"] == [{"verb": "POST", "path": "/code/search"}]
    assert registry["modules.read"]["http_routes"] == [{"verb": "GET", "path": "/modules/read"}]
    assert registry["metadata.relationship.verify"]["input_schema"]["required"] == ["base_id", "member"]
    assert registry["metadata.relationship.find"]["input_schema"]["properties"]["direction"]["enum"] == ["outgoing", "incoming", "either"]
    assert response["count"] == 185
    assert response["registry"] == diagnostics


def test_http_handler_dispatches_specialized_routes_from_registry(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[tuple[str, dict[str, Any]]] = []
    monkeypatch.setenv("ONEC_ADAPTER_SERVICE_TOKEN", "test-route-token")

    def fake_call_method(method: str, payload: dict[str, Any] | None) -> dict[str, Any]:
        calls.append((method, dict(payload or {})))
        return {"status": "ok", "method": method}

    monkeypatch.setattr(adapter_server, "call_method", fake_call_method)
    server = adapter_server.ThreadingHTTPServer(("127.0.0.1", 0), adapter_server.Handler)
    thread = adapter_server.threading.Thread(target=server.serve_forever, daemon=True)
    thread.start()
    base_url = f"http://127.0.0.1:{server.server_address[1]}"

    def request(path: str, body: dict[str, Any] | None = None) -> dict[str, Any]:
        data = json.dumps(body).encode("utf-8") if body is not None else None
        req = adapter_server.urllib.request.Request(
            base_url + path,
            data=data,
            method="POST" if body is not None else "GET",
            headers={
                "Authorization": "Bearer test-route-token",
                **({"Content-Type": "application/json"} if body is not None else {}),
            },
        )
        with adapter_server.urllib.request.urlopen(req, timeout=5) as response:
            return json.loads(response.read().decode("utf-8"))

    try:
        assert request("/methods?method=code.search")["method"] == "help.methods"
        assert request("/code/search", {"base_id": "upo_test", "query": "Найти"})["method"] == "code.search"
        assert request("/rpc", {"method": "metadata.kinds", "payload": {"base_id": "upo_test"}})["method"] == "metadata.kinds"
    finally:
        server.shutdown()
        server.server_close()
        thread.join(timeout=2)

    assert calls == [
        ("help.methods", {"method": "code.search"}),
        ("code.search", {"base_id": "upo_test", "query": "Найти"}),
        ("metadata.kinds", {"base_id": "upo_test"}),
    ]


@pytest.mark.parametrize(
    ("ref", "kind", "name"),
    [
        ("Справочники.Номенклатура", "Catalog", "Номенклатура"),
        ("СправочникСсылка.Номенклатура", "Catalog", "Номенклатура"),
        ("ДокументОбъект.Заказ", "Document", "Заказ"),
        ("РегистрыСведений.КурсыВалют", "InformationRegister", "КурсыВалют"),
        ("РегистрСведенийНаборЗаписей.КурсыВалют", "InformationRegister", "КурсыВалют"),
        ("Метаданные.HTTPСервисы.Интеграция", "HTTPService", "Интеграция"),
        ("cfg:ОбщиеМодули.Интеграция", "CommonModule", "Интеграция"),
    ],
)
def test_code_style_and_type_qualified_object_refs_resolve(ref: str, kind: str, name: str) -> None:
    assert adapter_server.parse_object_query(None, ref) == (kind, name)
    normalized = adapter_server.normalize_object_ref_payload({"ref": ref}, "test")
    assert normalized["kind"] == kind
    assert normalized["name"] == name


def test_nested_1c_path_keeps_object_identity_and_member_path() -> None:
    parsed = adapter_server.parse_1c_object_path("Документы.Заказ.Формы.ФормаДокумента.Элементы.КнопкаЗаписать")

    assert parsed["kind"] == "Document"
    assert parsed["name"] == "Заказ"
    assert parsed["member_path"] == ["Формы", "ФормаДокумента", "Элементы", "КнопкаЗаписать"]
    assert parsed["metadata_ref"] == "Документ.Заказ"
    assert parsed["code_ref"] == "Документы.Заказ"


def test_data_and_access_surfaces_share_code_style_object_resolution() -> None:
    data_payload = adapter_server.data_schema_selector_payload(
        {"base_id": "upo_test", "object_ref": "РегистрыСведений.КурсыВалют"}
    )
    normalized_data = adapter_server.normalize_object_selector_aliases(data_payload, "data.schema")
    normalized_access = adapter_server.normalize_object_selector_aliases(
        {"base_id": "upo_test", "ref": "Справочники.Номенклатура"},
        "access.object.roles",
    )

    assert normalized_data["kind"] == "InformationRegister"
    assert normalized_data["name"] == "КурсыВалют"
    assert normalized_access["kind"] == "Catalog"
    assert normalized_access["name"] == "Номенклатура"
    assert "access.object.roles" in adapter_server.OBJECT_SELECTOR_ALIAS_METHODS
    assert "access.object.subjects" in adapter_server.OBJECT_SELECTOR_ALIAS_METHODS


def test_data_methods_publish_name_first_selector_contracts() -> None:
    methods = {
        row["name"]: row
        for row in adapter_server.call_method("help.methods", {})["methods"]
    }

    assert adapter_server.DATA_OBJECT_SELECTOR_METHODS <= adapter_server.OBJECT_SELECTOR_ALIAS_METHODS
    for method in adapter_server.DATA_OBJECT_SELECTOR_METHODS:
        capabilities = methods[method]["selector_capabilities"]
        properties = methods[method]["input_schema"]["properties"]
        assert capabilities["requires_object_selector"] is True
        assert capabilities["accepts_object_ref"] is True
        assert capabilities["accepts_ordinal"] is False
        assert properties["object_ref"]["type"] == "string"
        assert properties["object_name"]["type"] == "string"
        assert "Справочники.Номенклатура" in properties["object_ref"]["description"]


def test_data_selector_keeps_record_ref_separate_from_russian_object_ref() -> None:
    record_ref = "A" * 32

    normalized = adapter_server.normalize_data_object_selector_aliases(
        {
            "base_id": "upo_test",
            "object_ref": "Справочники.Номенклатура",
            "ref": record_ref,
        },
        "data.get",
    )

    assert normalized["kind"] == "Catalog"
    assert normalized["name"] == "Номенклатура"
    assert normalized["object_ref"] == "Справочники.Номенклатура"
    assert normalized["ref"] == record_ref
    assert "guid" not in normalized


def test_data_get_dispatch_normalizes_object_name_without_consuming_record_ref(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    record_ref = "B" * 32
    captured: dict[str, Any] = {}

    def fake_data_read(payload: dict[str, Any], *, count_only: bool = False, method: str | None = None) -> dict[str, Any]:
        captured.update(payload)
        return {"schema": "onec_data_result.v1", "status": "ok", "rows": [], "method": method}

    monkeypatch.setattr(adapter_server, "data_read", fake_data_read)

    result = adapter_server.call_method(
        "data.get",
        {
            "base_id": "upo_test",
            "object_ref": "Справочники.Номенклатура",
            "record_ref": record_ref,
        },
    )

    assert result["status"] == "ok"
    assert captured["kind"] == "Catalog"
    assert captured["name"] == "Номенклатура"
    assert captured["record_ref"] == record_ref


def test_data_methods_reject_unsupported_ordinal_selector() -> None:
    result = adapter_server.call_method(
        "data.schema",
        {"base_id": "upo_test", "kind": "Catalog", "ordinal": 1},
    )

    assert result["status"] == "invalid_argument"
    assert result["argument"] == "ordinal"


def test_write_plan_path_accepts_code_roots_and_rejects_unknown_roots() -> None:
    parsed = adapter_server.metadata_write_plan_path_parts("ОбщиеМодули.Интеграция.Отправить")
    unknown = adapter_server.metadata_write_plan_path_parts("НеизвестныеОбъекты.Объект.Поле")

    assert parsed["is_full_path"] is True
    assert parsed["kind"] == "CommonModule"
    assert parsed["name"] == "Интеграция"
    assert parsed["path_kind"] == "module_routine"
    assert parsed["canonical_path"] == "ОбщийМодуль.Интеграция.Отправить"
    assert parsed["code_path"] == "ОбщиеМодули.Интеграция.Отправить"
    assert unknown["is_full_path"] is False
    assert unknown["reason"] == "unknown_object_kind"


def test_code_write_accepts_exact_extension_report_form_module_path() -> None:
    resolved = adapter_server.code_write_extension_form_module_path(
        "Расширение.фс_Отчеты1.Отчеты.фс_ПродовольственнаяКорзина.Формы.ФормаОтчетаПродкорзина.МодульФормы"
    )

    assert resolved == {
        "extension": "фс_Отчеты1",
        "object_type": "Report",
        "object_name": "фс_ПродовольственнаяКорзина",
        "form": "ФормаОтчетаПродкорзина",
        "canonical_path": "Отчет.фс_ПродовольственнаяКорзина.Формы.ФормаОтчетаПродкорзина.МодульФормы",
    }
    assert adapter_server.metadata_write_plan_path_parts(resolved["canonical_path"])["path_kind"] == "form_module"


def test_metadata_write_target_resolve_keeps_logical_target_separate_from_module_container(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "extension_filter_to_guid", lambda *_args, **_kwargs: ("11111111-1111-1111-1111-111111111111", None))
    monkeypatch.setattr(
        adapter_server,
        "saved_state_extension_form_active_route",
        lambda **_kwargs: ({
            "file_name": "active-form-payload",
            "owner": {"kind": "Report", "name": "фс_ПродовольственнаяКорзина"},
            "form": {"name": "ФормаОтчетаПродкорзина", "guid": "22222222-2222-2222-2222-222222222222"},
            "source": {"table": "ConfigCAS", "file_name": "active-form-payload"},
        }, None),
    )

    resolved = adapter_server.metadata_write_target_resolve({
        "base_id": "upo_test",
        "path": "Расширение.фс_Отчеты1.Отчеты.фс_ПродовольственнаяКорзина.Формы.ФормаОтчетаПродкорзина.МодульФормы",
    })

    assert resolved["status"] == "resolved"
    assert resolved["write_target"]["expected_origin"]["extension"] == "фс_Отчеты1"
    assert resolved["working_state"]["table"] == "ConfigCASSave"
    assert resolved["module_route"]["status"] == "unresolved"
    assert "module_ref" not in resolved["write_target"]


def test_metadata_write_target_resolve_rejects_old_fragment_from_unbound_container(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "extension_filter_to_guid", lambda *_args, **_kwargs: ("11111111-1111-1111-1111-111111111111", None))
    monkeypatch.setattr(
        adapter_server,
        "saved_state_extension_form_active_route",
        lambda **_kwargs: ({
            "source": {"table": "ConfigCAS", "file_name": "active-form-payload"},
            "owner": {"kind": "Report", "name": "фс_ПродовольственнаяКорзина"},
            "form": {"name": "ФормаОтчетаПродкорзина"},
        }, None),
    )
    monkeypatch.setattr(adapter_server, "read_module", lambda _payload: {"status": "ok", "text": "Процедура Тест()\nКонецПроцедуры", "content_sha1": "b" * 40})

    result = adapter_server.metadata_write_target_resolve({
        "base_id": "upo_test",
        "path": "Расширение.фс_Отчеты1.Отчеты.фс_ПродовольственнаяКорзина.Формы.ФормаОтчетаПродкорзина.МодульФормы",
        "old": "Вертикальная",
    })

    assert result["replacement_precondition"]["status"] == "old_not_found_in_confirmed_target"
    assert result["replacement_precondition"]["occurrences"] == 0


def test_metadata_object_forms_keeps_explicit_config_table_and_does_not_auto_switch_to_extension(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "fast_extension_object_exact_match",
        lambda *_args, **_kwargs: pytest.fail("An explicit Config route must not auto-switch to ConfigCAS"),
    )
    monkeypatch.setattr(adapter_server, "get_object", lambda *_args, **_kwargs: {"status": "not_found"})

    result = adapter_server.metadata_object_forms({
        "base_id": "upo_test",
        "kind": "Report",
        "name": "Тест",
        "table": "Config",
    })

    assert result["status"] == "not_found"


def test_get_object_explicit_config_never_falls_back_to_extension_definition(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "live_dbnames_records", lambda *_args, **_kwargs: ([], None))
    monkeypatch.setattr(adapter_server, "live_base_root_metadata_index", lambda *_args, **_kwargs: ([], []))
    monkeypatch.setattr(
        adapter_server,
        "metadata_extension_definition_matches",
        lambda *_args, **_kwargs: pytest.fail("Config lookup must not scan extension definitions"),
    )

    result = adapter_server.get_object("Report", "ТолькоВРасширении", base_id="upo_test", table="Config", timeout_seconds=5)

    assert result["status"] == "not_found"


def test_module_text_response_exposes_stable_text_preconditions() -> None:
    result = adapter_server.module_text_response("Процедура Тест()\nКонецПроцедуры", {"include_text": True})

    assert result["content_sha1"]
    assert result["selection_sha1"] == result["content_sha1"]


def test_code_write_rejects_mismatched_extension_path_and_origin() -> None:
    result = adapter_server.code_write({
        "base_id": "upo_test",
        "execution_mode": "apply",
        "old": "Вертикальная",
        "new": "Горизонтальная",
        "write_target": {
            "path": "Расширение.фс_Отчеты1.Отчеты.фс_ПродовольственнаяКорзина.Формы.ФормаОтчетаПродкорзина.МодульФормы",
            "expected_origin": {"extension": "другоеРасширение"},
            "expected_sha1": "a" * 40,
        },
    })

    assert result["status"] == "invalid_argument"
    assert result["argument"] == "write_target.expected_origin.extension"


def test_code_search_excludes_unbound_hit_when_extension_form_scope_is_exact(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "code_saved_state_common_form_module_search", lambda *_args, **_kwargs: None)
    monkeypatch.setattr(
        adapter_server,
        "search_modules",
        lambda _payload: {
            "status": "ok",
            "matches": [{
                "owner": {"kind": "Report", "name": "фс_ПродовольственнаяКорзина"},
                "module": {"form": {"name": "ФормаОтчетаПродкорзина"}},
                "origin": {"extension": {"name": "другоеРасширение"}},
                "snippet": {"text": "Вертикальная", "offset": 0},
            }],
            "counts": {"complete": True},
        },
    )

    result = adapter_server.code_search({
        "base_id": "upo_test",
        "extension": "фс_Отчеты1",
        "kind": "Report",
        "name": "фс_ПродовольственнаяКорзина",
        "form": "ФормаОтчетаПродкорзина",
        "query": "Вертикальная",
    })

    assert result["status"] == "not_found"
    assert result["counts"]["excluded_unbound_matches"] == 1


def test_form_details_uses_configcas_for_extension_when_table_is_not_explicit(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_forms(payload: dict[str, Any]) -> dict[str, Any]:
        seen["table"] = payload.get("table")
        return {"status": "not_found"}

    monkeypatch.setattr(adapter_server, "metadata_object_forms", fake_forms)
    result = adapter_server.metadata_object_form_details({
        "base_id": "upo_test",
        "extension": "test2",
        "kind": "Report",
        "name": "Тест",
        "form": "Форма",
    })

    assert result["status"] == "not_found"
    assert seen["table"] == "ConfigCAS"


def test_root_kind_routes_match_xml_exported_configuration() -> None:
    assert adapter_server.ROOT_APPLICATION_CLASS_KIND["1c57eabe-7349-44b3-b1de-ebfeab67b47d"] == "CommandGroup"
    assert adapter_server.ROOT_APPLICATION_CLASS_KIND["36a8e346-9aaa-4af9-bdbd-83be3c177977"] == "DocumentNumerator"
    assert adapter_server.ROOT_COLLECTION_KIND["0fffc09c-8f4c-47cc-b41c-8d5c5a221d79"] == "HTTPService"
    assert adapter_server.ROOT_COLLECTION_KIND["8657032e-7740-4e1d-a3ba-5dd6e8afb78f"] == "WebService"
    assert adapter_server.ROOT_COLLECTION_KIND["cc9df798-7c94-4616-97d2-7aa0b7bc515e"] == "XDTOPackage"
    assert adapter_server.ROOT_COLLECTION_KIND["5274d9fc-9c3a-4a71-8f5e-a0db8ab23de5"] == "ExternalDataSource"
    assert adapter_server.ROOT_COLLECTION_KIND["bf3420b0-f6f9-41a0-b83a-fe9d4ab0b65d"] == "IntegrationService"
    assert adapter_server.ROOT_COLLECTION_KIND["a7641777-7813-45c6-96ef-9d51587a6ac6"] == "Interface"
    assert "DocumentNumerator" in adapter_server.TOP_LEVEL_METADATA_KINDS
    assert "IntegrationService" in adapter_server.TOP_LEVEL_METADATA_KINDS
    assert "Configuration" in adapter_server.TOP_LEVEL_METADATA_KINDS
    assert adapter_server.canonical_kind("FilterCriterion") == "SelectionCriterion"
    assert adapter_server.public_metadata_row({"kind": "DocumentNumerator", "name": "Нумератор", "guid": "x"})["ref"] == "DocumentNumerator.Нумератор"
    assert adapter_server.metadata_cache_public_row({"kind": "IntegrationService", "name": "Обмен", "guid": "x"})["ref"] == "IntegrationService.Обмен"


def test_every_kind_specific_sql_decoder_is_advertised_by_capabilities() -> None:
    for kind in adapter_server.SPECIAL_PROPERTY_KINDS:
        capabilities = adapter_server.KIND_CAPABILITIES[kind]
        assert "properties" in capabilities, kind
        assert "special_details" in capabilities, kind


@pytest.mark.parametrize(
    ("capability", "category", "direct_kinds"),
    [
        ("commands", "Command", {"CommandGroup"}),
        ("forms", "Form", set()),
        ("templates", "Template", {"CommonTemplate"}),
    ],
)
def test_advertised_related_capability_has_a_live_collection_rule(
    capability: str,
    category: str,
    direct_kinds: set[str],
) -> None:
    advertised = {kind for kind, capabilities in adapter_server.KIND_CAPABILITIES.items() if capability in capabilities}
    implemented = {
        kind
        for kind, rules in adapter_server.RELATED_SECTION_RULES.items()
        if any(rule.get("category") == category for rule in rules)
    } | direct_kinds

    assert advertised == implemented


def test_corrected_related_capabilities_cover_calculation_register_without_false_legacy_claims() -> None:
    assert {"forms", "templates", "commands"} <= set(adapter_server.KIND_CAPABILITIES["CalculationRegister"])
    assert "commands" in adapter_server.KIND_CAPABILITIES["CommandGroup"]
    assert "commands" not in adapter_server.KIND_CAPABILITIES["Interface"]
    assert "templates" not in adapter_server.KIND_CAPABILITIES["ChartOfAccounts"]
    assert "templates" not in adapter_server.KIND_CAPABILITIES["ChartOfCalculationTypes"]


def test_related_selectors_follow_corrected_capabilities_and_keep_public_names() -> None:
    calculation = adapter_server.object_related_selectors(
        "upo_test",
        {
            "kind": "CalculationRegister",
            "name": "Начисления",
            "guid": "11111111-1111-1111-1111-111111111111",
            "ref": "CalculationRegister.Начисления",
        },
    )
    interface = adapter_server.object_related_selectors(
        "upo_test",
        {
            "kind": "Interface",
            "name": "Основной",
            "guid": "22222222-2222-2222-2222-222222222222",
            "ref": "Interface.Основной",
        },
    )

    assert calculation["forms"]["ref"] == "CalculationRegister.Начисления"
    assert calculation["templates"]["name"] == "Начисления"
    assert calculation["commands"]["method"] == "metadata.object.commands"
    command_group = adapter_server.object_related_selectors(
        "upo_test",
        {
            "kind": "CommandGroup",
            "name": "Сервис",
            "guid": "33333333-3333-3333-3333-333333333333",
            "ref": "CommandGroup.Сервис",
        },
    )

    assert command_group["commands"]["ref"] == "CommandGroup.Сервис"
    assert set(interface) == {"card", "full"}


def test_command_group_commands_follow_reverse_common_command_relation(monkeypatch: pytest.MonkeyPatch) -> None:
    group_guid = "11111111-1111-1111-1111-111111111111"
    first_guid = "22222222-2222-2222-2222-222222222222"
    second_guid = "33333333-3333-3333-3333-333333333333"

    def command_tree(command_group_guid: str) -> dict[str, Any]:
        atom = lambda value: {"type": "atom", "value": str(value)}
        seq = lambda *items: {"type": "list", "items": list(items)}
        root = seq()

        def put(path: tuple[int, ...], value: dict[str, Any]) -> None:
            node = root
            for index in path[:-1]:
                items = node.setdefault("items", [])
                while len(items) <= index:
                    items.append(seq())
                node = items[index]
            items = node.setdefault("items", [])
            while len(items) <= path[-1]:
                items.append(seq())
            items[path[-1]] = value

        put((1, 1, 2, 7, 1), atom(command_group_guid))
        return root

    payloads = {
        first_guid: serialize_brace_tree(command_tree(group_guid)).encode("utf-8"),
        second_guid: serialize_brace_tree(command_tree("44444444-4444-4444-4444-444444444444")).encode("utf-8"),
    }
    monkeypatch.setattr(
        adapter_server,
        "list_objects",
        lambda *_args, **_kwargs: {
            "status": "ok",
            "objects": [
                {"kind": "CommonCommand", "guid": first_guid, "name": "Первая", "synonym": "Первая команда", "ref": "CommonCommand.Первая"},
                {"kind": "CommonCommand", "guid": second_guid, "name": "Вторая", "synonym": "Вторая команда", "ref": "CommonCommand.Вторая"},
            ],
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_files_bytes",
        lambda *_args, **_kwargs: (payloads, {"database": "test"}, None),
    )

    result = adapter_server.metadata_command_group_commands(
        base_id="upo_test",
        object_card={"kind": "CommandGroup", "guid": group_guid, "name": "Сервис", "ref": "CommandGroup.Сервис"},
        requested_command=None,
        include_storage=False,
        table="Config",
        max_commands=200,
        timeout_seconds=30,
    )

    assert result["status"] == "ok"
    assert result["counts"]["scanned_common_commands"] == 2
    assert result["counts"]["matched_common_commands"] == 1
    assert result["commands"][0]["ref"] == "CommonCommand.Первая"
    assert result["commands"][0]["module"]["read_selector"]["ref"] == "CommonCommand.Первая"
    assert result["capabilities"]["reverse_relation"] == "CommonCommand.group"
    assert all("storage" not in command for command in result["commands"])


def test_metadata_kinds_combines_sequence_dbnames_and_interface_root_discovery(monkeypatch: pytest.MonkeyPatch) -> None:
    sequence_guid = "11111111-1111-1111-1111-111111111111"
    interface_guid = "22222222-2222-2222-2222-222222222222"

    class SequenceRecord:
        guid = sequence_guid
        storage_role = "Sequence"

    monkeypatch.setattr(adapter_server, "live_dbnames_records", lambda *_args, **_kwargs: ([SequenceRecord()], None))
    monkeypatch.setattr(
        adapter_server,
        "live_base_root_metadata_index",
        lambda *_args, **_kwargs: (
            [
                {
                    "guid": interface_guid,
                    "kind": "Interface",
                    "kind_ru": "Интерфейс",
                    "public_kind": "interface",
                    "source": "base",
                }
            ],
            [],
        ),
    )

    result = adapter_server.get_kinds("upo_test")

    assert result["status"] == "ok"
    assert result["internal_kind_counts"]["Sequence"] == 1
    assert result["internal_kind_counts"]["Interface"] == 1
    assert {item["kind"]: item["count"] for item in result["kinds"]}["sequence"] == 1
    assert {item["kind"]: item["count"] for item in result["kinds"]}["interface"] == 1


def test_metadata_cache_rebuild_merges_root_only_objects(monkeypatch: pytest.MonkeyPatch) -> None:
    root_guid = "11111111-1111-1111-1111-111111111111"
    stored: list[dict[str, Any]] = []
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda _base_id: ({"server": "sql", "database": "base"}, None))
    monkeypatch.setattr(adapter_server, "live_dbnames_records", lambda *_args, **_kwargs: ([], None))
    monkeypatch.setattr(adapter_server, "metadata_guid_index_migrate_legacy_types", lambda _config: 0)
    monkeypatch.setattr(
        adapter_server,
        "live_base_root_metadata_index",
        lambda *_args, **_kwargs: ([{"guid": root_guid, "kind": "SettingsStorage", "kind_ru": "ХранилищеНастроек", "public_kind": "settings_storage", "source": "base"}], []),
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_files_bytes",
        lambda *_args, **_kwargs: ({root_guid: b"payload"}, [], None),
    )
    monkeypatch.setattr(
        adapter_server,
        "config_identity_from_bytes",
        lambda _data: {"guid": root_guid, "name": "Настройки", "synonyms": {"ru": "Настройки"}},
    )
    monkeypatch.setattr(adapter_server, "metadata_cache_upsert", lambda _config, row: stored.append(dict(row)))
    monkeypatch.setattr(adapter_server, "generated_type_records_from_bytes", lambda *_args, **_kwargs: [])
    monkeypatch.setattr(adapter_server, "live_config_file_name_page_after", lambda *_args, **_kwargs: [])

    result = adapter_server.metadata_cache_rebuild_base("upo_test")

    assert result["status"] == "ok"
    assert result["counts"]["candidates"] == 1
    assert result["counts"]["updated"] == 1
    assert stored[0]["kind"] == "SettingsStorage"
    assert stored[0]["name"] == "Настройки"


def test_style_item_sql_details_decodes_color_font_and_border() -> None:
    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    identity = seq(atom(3), seq(atom(1), atom(0), atom("11111111-1111-1111-1111-111111111111")), atom("Test"))

    color_tree = seq(atom(1), seq(atom(3), atom(0), seq(atom("#"), atom("9cd510c7-abfc-11d4-9434-004095e12fc7"), atom(2), seq(atom(3), atom(0), seq(atom(4625920)))), identity), atom(0))
    color = adapter_server.style_item_sql_details(color_tree)
    assert color["value_type"] == "Color"
    assert color["value"]["value"] == "#009646"

    font_payload = seq(atom(7), atom(0), atom(575), atom(100), atom(0), atom(0), atom(0), atom(700), atom(1), atom(0), atom(0), atom(0), atom(0), atom(0), atom(0), atom(0), atom("Arial"), atom(1), atom(100))
    font_tree = seq(atom(1), seq(atom(3), atom(1), seq(atom("#"), atom("9cd510c8-abfc-11d4-9434-004095e12fc7"), atom(1), font_payload, atom(0)), identity), atom(0))
    font = adapter_server.style_item_sql_details(font_tree)
    assert font["value_type"] == "Font"
    assert font["value"] == {"kind": "absolute", "face_name": "Arial", "height": 10.0, "bold": True, "italic": True, "underline": False, "strikeout": False, "scale": 100}

    border_payload = seq(atom(3), atom(0), seq(atom(0)), atom(1), atom(1), atom(0), atom("48312c09-257f-4b29-b280-284dd89efc1e"))
    border_tree = seq(atom(1), seq(atom(3), atom(2), seq(atom("#"), atom("4d10ca00-111a-4d43-9c96-92cd773716de"), atom(1), border_payload, atom(0)), identity), atom(0))
    border = adapter_server.style_item_sql_details(border_tree)
    assert border["value_type"] == "Border"
    assert border["value"] == {"style": "Single", "width": 1}


def test_language_and_common_picture_sql_properties() -> None:
    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    identity = seq(atom(3), seq(atom(1), atom(0), atom("11111111-1111-1111-1111-111111111111")), atom("Test"))
    assert adapter_server.language_sql_details(seq(atom(1), seq(atom(0), identity, atom("ru")), atom(0))) == {"language_code": "ru"}
    picture = adapter_server.common_picture_sql_details(seq(atom(1), seq(atom(4), identity, atom(1), atom(0)), atom(0)), None)
    assert picture["availability_for_choice"] is True
    assert picture["availability_for_appearance"] is False
    assert picture["binary"]["status"] == "source_missing"


@pytest.mark.parametrize(
    ("content", "media_type"),
    [
        (b"\x89PNG\r\n\x1a\nrest", "image/png"),
        (b"<svg width=\"16\"></svg>", "image/svg+xml"),
        (b"\xef\xbb\xbf<?xml version=\"1.0\"?><svg></svg>", "image/svg+xml"),
        (b"PK\x03\x04picture-package", "application/zip"),
        (b"\x00\x00\x01\x00\x01\x00icon", "image/x-icon"),
    ],
)
def test_binary_part_summary_recognizes_picture_formats(content: bytes, media_type: str) -> None:
    encoded = base64.b64encode(content).decode("ascii")
    stored = compress_payload(f'{{1,{{#base64:{encoded}}}}}'.encode("utf-8-sig"), "raw_deflate")
    result = adapter_server.binary_part_summary(stored)
    assert result["status"] == "ok"
    assert result["media_type"] == media_type
    assert result["bytes"] == len(content)


def test_xdto_package_xml_details_decodes_types_properties_and_enums() -> None:
    xml = b'''<?xml version="1.0" encoding="UTF-8"?>
    <package xmlns="http://v8.1c.ru/8.1/xdto" xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:test">
      <import namespace="urn:base"/>
      <valueType name="State" base="xs:string"><enumeration>Open</enumeration><enumeration>Closed</enumeration></valueType>
      <objectType name="Order"><property name="Code" type="xs:string" lowerBound="0"/><property name="Child"><typeDef type="ObjectType"><property name="Value" type="xs:int"/></typeDef></property></objectType>
    </package>'''
    result = adapter_server.xdto_package_xml_details(compress_payload(xml, "raw_deflate"))
    assert result["status"] == "ok"
    assert result["namespace"] == "urn:test"
    assert result["imports"] == ["urn:base"]
    assert result["counts"] == {"types": 2, "object_types": 1, "value_types": 1, "properties": 3, "enumerations": 2, "inline_type_definitions": 1, "imports": 1}
    assert result["types"][0]["enumerations"][0]["value"] == "Open"
    assert result["types"][1]["properties"][1]["type_definition"]["properties"][0]["name"] == "Value"


def test_ws_reference_sql_details_decodes_wsdl_and_xsd_streams() -> None:
    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    tree = seq(atom(1), seq(atom(2), seq(atom("https://example.test/service?wsdl"), atom(0)), atom("identity"), atom("11111111-1111-1111-1111-111111111111"), atom("22222222-2222-2222-2222-222222222222")), atom(0))
    wsdl = b'''<definitions xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" name="TestService" targetNamespace="urn:test"><message name="PingIn"><part name="parameters" element="tns:Ping"/></message><portType name="ITest"><operation name="Ping"><input message="tns:PingIn"/><output message="tns:PingOut"/></operation></portType><binding name="TestBinding" type="tns:ITest"><operation name="Ping"><soap:operation soapAction="urn:test/Ping"/></operation></binding><service name="TestService"><port name="TestPort" binding="tns:TestBinding"><soap:address location="https://example.test/service"/></port></service></definitions>'''
    xsd = b'''<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:test"><xs:complexType name="Ping"><xs:sequence><xs:element name="Value" type="xs:string"/></xs:sequence></xs:complexType><xs:element name="Ping" type="tns:Ping"/></xs:schema>'''
    payload = b"prefix" + stream_header(len(wsdl)) + wsdl + stream_header(len(xsd)) + xsd
    result = adapter_server.ws_reference_sql_details(tree, compress_payload(payload, "raw_deflate"))
    assert result["status"] == "ok"
    assert result["location_url"] == "https://example.test/service?wsdl"
    assert result["counts"] == {"streams": 2, "xml_streams": 2, "wsdl_definitions": 1, "schemas": 1, "operations": 1, "services": 1, "ports": 1}
    assert result["definitions"][0]["port_types"][0]["operations"][0]["soap_action"] == "urn:test/Ping"
    assert result["schemas"][0]["types"][0]["members"][0]["name"] == "Value"


def test_sql_only_decoders_cover_document_numerator_and_integration_channels() -> None:
    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    identity = seq(atom(3), seq(atom(1), atom(0), atom("00000000-0000-0000-0000-000000000001")), atom("Test"), seq(atom(1), atom("ru"), atom("Тест")))
    numerator_tree = seq(atom(1), seq(atom(3), identity, atom(1), atom(11), atom(1), atom(1), atom(1)), atom(0))

    numerator = adapter_server.document_numerator_sql_details(numerator_tree, include_storage=False)

    assert numerator["number_type"] == "String"
    assert numerator["number_length"] == 11
    assert numerator["number_allowed_length"] == "Variable"
    assert numerator["number_periodicity"] == "Year"
    assert numerator["check_unique"] is True
    assert all("storage" not in item for item in numerator["properties"])

    chart_header = [atom(35), identity]
    while len(chart_header) <= 57:
        chart_header.append(atom(0))
    chart_header[24] = atom(1)
    chart_header[25] = atom(5)
    chart_header[26] = atom(1)
    chart_header[27] = atom(0)
    chart_header[30] = atom(100)
    chart_header[32] = atom("11111111-1111-1111-1111-111111111111")
    chart_header[33] = atom("22222222-2222-2222-2222-222222222222")
    chart_header[34] = atom("00000000-0000-0000-0000-000000000000")
    chart_header[48] = seq(atom(1), atom("ru"), atom("Вид расчета"))
    chart_header[49] = seq(atom(0))
    chart_header[50] = seq(atom(1), atom("ru"), atom("Виды расчета"))
    chart_header[57] = atom(0)
    chart = adapter_server.chart_of_calculation_types_sql_details(seq(atom(1), seq(*chart_header)), include_storage=False)

    assert chart["use_standard_commands"] is True
    assert chart["code_length"] == 5
    assert chart["code_type"] == "String"
    assert chart["code_allowed_length"] == "Variable"
    assert chart["description_length"] == 100
    assert chart["default_object_form"]["guid"] == "11111111-1111-1111-1111-111111111111"
    assert chart["default_list_form"]["guid"] == "22222222-2222-2222-2222-222222222222"
    assert chart["default_choice_form"] is None
    assert chart["object_presentation"] == {"ru": "Вид расчета"}
    assert chart["list_presentation"] == {"ru": "Виды расчета"}
    assert chart["action_period_use"] is False
    assert len(chart["properties"]) == 17
    assert all("storage" not in item for item in chart["properties"])

    register_header = [atom(21)]
    while len(register_header) <= 31:
        register_header.append(atom(0))
    register_header[16] = atom(2)
    register_header[17] = atom(0)
    register_header[18] = atom(0)
    register_header[19] = atom("00000000-0000-0000-0000-000000000000")
    register_header[20] = atom("00000000-0000-0000-0000-000000000000")
    register_header[22] = atom("33333333-3333-3333-3333-333333333333")
    register_header[24] = atom(1)
    register_header[25] = atom(0)
    register_header[26] = atom(1)
    register_header[27] = atom(0)
    register_header[28] = seq(atom(1), atom("ru"), atom("Расчеты"))
    register_header[30] = seq(atom(0))
    register_header[31] = seq(atom(0))
    register = adapter_server.calculation_register_sql_details(seq(atom(1), seq(*register_header)), include_storage=False)

    assert register["periodicity"] == "Month"
    assert register["action_period"] is False
    assert register["base_period"] is False
    assert register["chart_of_calculation_types"]["guid"] == "33333333-3333-3333-3333-333333333333"
    assert register["use_standard_commands"] is True
    assert register["include_help_in_contents"] is False
    assert register["data_lock_control_mode"] == "Managed"
    assert register["full_text_search"] == "DontUse"
    assert register["list_presentation"] == {"ru": "Расчеты"}
    assert len(register["properties"]) == 13
    collections = {item["path"]: item for item in register["child_collections"]}
    assert collections["3"]["role"] == "Attribute"
    assert collections["4"]["role"] == "Recalculation"
    assert collections["5"]["role"] == "Template"
    assert collections["6"]["role"] == "Resource"
    assert collections["7"]["role"] == "Form"
    assert collections["8"]["role"] == "Command"
    assert collections["9"]["role"] == "Dimension"
    assert all(item["status"] == "classified" for item in collections.values())
    assert all("possible_roles" not in item for item in collections.values())
    assert collections["3"]["collection_ordinal"] == 1
    assert collections["9"]["collection_ordinal"] == 7

    semantic_tree = seq(atom(1), *(seq(atom(f"marker-{index}"), atom(0)) for index in range(1, 10)))
    semantic = decode_config_semantic(semantic_tree, kind="CalculationRegister", include_generic=False)
    assert [(section["path"], section["category"]) for section in semantic["sections"]] == [
        ("3", "Attribute"),
        ("4", "Recalculation"),
        ("6", "Resource"),
        ("9", "Dimension"),
    ]

    channel_identity = seq(atom(3), seq(atom(1), atom(0), atom("00000000-0000-0000-0000-000000000002")), atom("input"), seq(atom(1), atom("ru"), atom("Вход")))
    channel_record = seq(atom(1), channel_identity, atom("type-id"), atom("value-id"), atom("external.input"), atom("ProcessInput"), atom(1), atom(0))
    integration_tree = seq(atom(1), seq(), atom(1), seq(atom("class-id"), atom(1), seq(channel_record, atom(0))))

    integration = adapter_server.integration_service_sql_details(integration_tree, include_storage=False)

    assert integration["counts"] == {"channels": 1, "declared_channels": 1}
    assert integration["channels"][0]["name"] == "input"
    assert integration["channels"][0]["synonyms"] == {"ru": "Вход"}
    assert integration["channels"][0]["message_direction"] == "Receive"
    assert integration["channels"][0]["transactioned"] is False
    assert "storage" not in integration["channels"][0]

    configuration_tree = seq()

    def put(path: tuple[int, ...], value: dict[str, Any]) -> None:
        node = configuration_tree
        for index in path[:-1]:
            items = node.setdefault("items", [])
            while len(items) <= index:
                items.append(seq())
            node = items[index]
        items = node.setdefault("items", [])
        while len(items) <= path[-1]:
            items.append(seq())
        items[path[-1]] = value

    put((3, 1, 1, 1, 1, 2), atom("TestConfiguration"))
    put((3, 1, 1, 1, 1, 3), seq(atom(1), atom("ru"), atom("Тестовая конфигурация")))
    put((3, 1, 1, 4), seq(atom(1), atom("ru"), atom("Подробно")))
    put((3, 1, 1, 5), seq(atom(1), atom("ru"), atom("Кратко")))
    put((3, 1, 1, 6), seq(atom(1), atom("ru"), atom("Copyright")))
    put((3, 1, 1, 7), seq(atom(1), atom("ru"), atom("https://vendor.example")))
    put((3, 1, 1, 8), seq(atom(1), atom("ru"), atom("https://configuration.example")))
    put((3, 1, 1, 14), atom("Vendor"))
    put((3, 1, 1, 15), atom("1.2.3"))
    put((3, 1, 1, 16), atom("https://updates.example"))

    configuration = adapter_server.configuration_sql_details(configuration_tree, include_storage=False)

    assert configuration["name"] == "TestConfiguration"
    assert configuration["synonyms"] == {"ru": "Тестовая конфигурация"}
    assert configuration["vendor"] == "Vendor"
    assert configuration["version"] == "1.2.3"
    assert configuration["counts"] == {"decoded_properties": 10, "declared_properties": 10}
    assert all("storage" not in item for item in configuration["properties"])

    schedule_tree = seq(
        atom("00010101000000"), atom("00010101000000"), atom("00010101000001"), atom("00010101000000"), atom("00010101000000"),
        atom(0), atom(0), atom(0), atom(7), atom(1), atom(2), atom(3), atom(4), atom(5), atom(6), atom(7),
        atom(0), atom(0), atom(12), atom(1), atom(2), atom(3), atom(4), atom(5), atom(6), atom(7), atom(8), atom(9), atom(10), atom(11), atom(12),
        atom(1), atom(1), atom(0),
    )

    schedule = adapter_server.scheduled_job_sql_schedule(schedule_tree, include_storage=False)

    assert schedule["status"] == "ok"
    assert schedule["begin_date"] == "0001-01-01"
    assert schedule["begin_time"] == "00:00:01"
    assert schedule["week_days"] == [1, 2, 3, 4, 5, 6, 7]
    assert schedule["months"] == list(range(1, 13))
    assert schedule["weeks_period"] == 1
    assert schedule["days_repeat_period"] == 1
    assert "storage" not in schedule

    command_group_tree = seq(
        atom(1),
        seq(
            atom(3),
            seq(atom(4), atom(1), seq(atom(0), atom("00000000-0000-0000-0000-000000000003")), atom(""), atom(-1), atom(-1), atom(0), atom(0), atom("")),
            atom(8),
            atom(1),
            seq(atom(1), atom("ru"), atom("Подсказка")),
        ),
        atom(0),
    )

    command_group = adapter_server.command_group_sql_details(command_group_tree, include_storage=False)

    assert command_group["category"] == "FormCommandBar"
    assert command_group["representation"] == "Picture"
    assert command_group["tool_tip"] == {"ru": "Подсказка"}
    assert command_group["picture"]["guid"] == "00000000-0000-0000-0000-000000000003"
    assert command_group["load_transparent"] is False
    assert "storage" not in command_group


def test_scheduled_job_schedule_write_edits_use_named_fields_and_preserve_collection_shape() -> None:
    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    tree = seq(
        atom("00010101000000"), atom("00010101000000"), atom("00010101000001"), atom("00010101000000"), atom("00010101000000"),
        atom(0), atom(0), atom(0), atom(2), atom(1), atom(5),
        atom(0), atom(0), atom(2), atom(1), atom(12),
        atom(1), atom(1),
    )

    result = adapter_server.scheduled_job_schedule_write_edits(
        tree,
        {
            "begin_time": "08:30:00",
            "completion_interval": 15,
            "week_days": [2, 6],
            "months": [2, 11],
        },
    )

    assert result["status"] == "ok"
    assert result["requested"] == {
        "begin_time": "08:30:00",
        "completion_interval": 15,
        "week_days": [2, 6],
        "months": [2, 11],
    }
    edits = {edit["path"]: edit for edit in result["edits"]}
    assert edits["2"]["value"] == "00010101083000"
    assert edits["5"]["value"] == "15"
    assert [edits[path]["value"] for path in ("9", "10")] == ["2", "6"]
    assert [edits[path]["value"] for path in ("14", "15")] == ["2", "11"]
    assert all("expected_old" in edit for edit in result["edits"])


def test_scheduled_job_schedule_write_rebuilds_tree_for_collection_resize() -> None:
    atom = lambda value: {"type": "atom", "value": str(value)}
    tree = {
        "type": "list",
        "items": [
            atom("00010101000000"), atom("00010101000000"), atom("00010101000000"), atom("00010101000000"), atom("00010101000000"),
            atom(0), atom(0), atom(0), atom(2), atom(1), atom(5),
            atom(0), atom(0), atom(1), atom(1),
            atom(1), atom(1),
        ],
    }

    result = adapter_server.scheduled_job_schedule_write_edits(
        tree,
        {"begin_time": "09:30:00", "week_days": [1, 2, 3], "months": [1, 6, 12]},
    )

    assert result["status"] == "ok"
    assert result["counts"]["resized_collections"] == 2
    assert len(result["edits"]) == 1
    edit = result["edits"][0]
    assert edit["resized_collections"] == ["months", "week_days"]
    assert edit["old_counts"] == {"week_days": 2, "months": 1}
    assert edit["new_counts"] == {"week_days": 3, "months": 3}
    rebuilt = adapter_server.scheduled_job_sql_schedule(edit["replace_root"], include_storage=False)
    assert rebuilt["begin_time"] == "09:30:00"
    assert rebuilt["week_days"] == [1, 2, 3]
    assert rebuilt["months"] == [1, 6, 12]


def test_metadata_scheduled_job_schedule_write_resolves_public_1c_ref_to_configsave(monkeypatch: pytest.MonkeyPatch) -> None:
    guid = "11111111-1111-1111-1111-111111111111"
    atom = lambda value: {"type": "atom", "value": str(value)}
    tree = {
        "type": "list",
        "items": [
            atom("00010101000000"), atom("00010101000000"), atom("00010101000000"), atom("00010101000000"), atom("00010101000000"),
            atom(0), atom(0), atom(0), atom(1), atom(1),
            atom(0), atom(0), atom(1), atom(1),
            atom(1), atom(1),
        ],
    }
    captured: dict[str, Any] = {}
    monkeypatch.setattr(
        adapter_server,
        "get_object",
        lambda kind, selector, **_kwargs: {
            "status": "ok",
            "object": {"kind": kind, "name": "ОбменДанными", "guid": guid, "ref": "ScheduledJob.ОбменДанными"},
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda *_args, **_kwargs: (b"schedule", {"database": "test"}, None),
    )
    monkeypatch.setattr(adapter_server, "parse_config_tree_from_bytes", lambda _data: tree)

    def fake_changes_propose(payload: dict[str, Any]) -> dict[str, Any]:
        captured.update(payload)
        return {
            "schema": "onec_change_proposal.v1",
            "status": "accepted_for_review",
            "source": payload["source"],
            "original": {"sha1": "old"},
            "encoded": {"sha1": "new"},
        }

    monkeypatch.setattr(adapter_server, "changes_propose", fake_changes_propose)

    read_only = adapter_server.metadata_scheduled_job_schedule_write(
        {
            "base_id": "upo_test",
            "target": {"kind": "schedule", "ref": "РегламентныеЗадания.ОбменДанными"},
            "schedule": {"begin_time": "09:15:00"},
        }
    )

    assert read_only["status"] == "blocked"
    assert read_only["next_call"]["method"] == "metadata.write"
    assert read_only["next_call"]["params"]["target"]["ref"] == "ScheduledJob.ОбменДанными"
    assert read_only["next_call"]["params"]["allow_saved_state_write"] is True
    assert "payload" not in read_only["next_call"]

    result = adapter_server.metadata_scheduled_job_schedule_write(
        {
            "base_id": "upo_test",
            "target": {"kind": "schedule", "ref": "РегламентныеЗадания.ОбменДанными"},
            "schedule": {"begin_time": "09:15:00"},
            "allow_saved_state_write": True,
            "execution_mode": "plan",
        }
    )

    assert result["status"] == "planned"
    assert result["object"]["ref"] == "ScheduledJob.ОбменДанными"
    assert captured["source"]["table"] == "ConfigSave"
    assert captured["source"]["file_name"] == f"{guid}.0"
    assert captured["edits"] == [{"path": "2", "value": "00010101091500", "node_type": "atom", "expected_old": "00010101000000"}]
    assert captured["include_payload"] is False


def test_metadata_write_routes_schedule_target_without_changing_existing_form_module_routes(monkeypatch: pytest.MonkeyPatch) -> None:
    captured: dict[str, Any] = {}
    monkeypatch.setattr(adapter_server, "normalize_repository_write_context", lambda payload, _method: (dict(payload), None))
    monkeypatch.setattr(adapter_server, "resolve_write_gate_context", lambda payload: payload)
    monkeypatch.setattr(adapter_server, "repository_apply_gate", lambda *_args, **_kwargs: None)
    monkeypatch.setattr(adapter_server, "metadata_write_save_first_payload", lambda payload, _mode: payload)
    monkeypatch.setattr(
        adapter_server,
        "metadata_scheduled_job_schedule_write",
        lambda payload: captured.update(payload) or {"schema": "onec_scheduled_job_schedule_write.v1", "status": "planned"},
    )

    result = adapter_server.call_method(
        "metadata.write",
        {
            "base_id": "upo_test",
            "target": {"kind": "schedule", "ref": "РегламентныеЗадания.ОбменДанными"},
            "schedule": {"begin_time": "09:15:00"},
            "execution_mode": "plan",
        }
    )

    assert result["status"] == "planned"
    assert result["routed_method"] == "metadata.write:scheduled_job_schedule"
    assert result["target_kind"] == "schedule"
    assert captured["target"]["ref"] == "РегламентныеЗадания.ОбменДанными"


def test_metadata_write_preflight_routes_named_scheduled_job_read_only(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "normalize_repository_write_context", lambda payload, _method: (dict(payload), None))
    monkeypatch.setattr(adapter_server, "resolve_write_gate_context", lambda payload: payload)
    monkeypatch.setattr(
        adapter_server,
        "metadata_scheduled_job_schedule_write",
        lambda _payload: {
            "schema": "onec_scheduled_job_schedule_write.v1",
            "status": "planned",
            "object": {
                "kind": "ScheduledJob",
                "name": "ОбменДанными",
                "guid": "11111111-1111-1111-1111-111111111111",
                "ref": "ScheduledJob.ОбменДанными",
            },
            "source": {"kind": "live_metadata"},
            "proposal": {"original": {"sha1": "abc"}},
        },
    )
    monkeypatch.setattr(adapter_server.repository_control, "write_gate", lambda _payload: {"status": "ok", "allowed": True})
    monkeypatch.setattr(adapter_server, "live_sql_support_gate", lambda _payload: {"status": "ok", "allowed": True})

    result = adapter_server.metadata_write_preflight(
        {
            "base_id": "upo_test",
            "target": {"kind": "schedule", "ref": "РегламентныеЗадания.ОбменДанными"},
            "schedule": {"begin_time": "09:15:00"},
        }
    )

    assert result["status"] == "ready"
    assert result["allowed"] is True
    assert result["target"]["ref"] == "ScheduledJob.ОбменДанными"
    assert result["route"]["writer"] == "metadata.write:scheduled_job_schedule"
    assert result["guards"]["expected_sha1"] == "abc"
    assert result["diagnostics"]["read_only"] is True


def test_service_module_roles_and_full_stream_selection(monkeypatch: pytest.MonkeyPatch) -> None:
    guid = "10000000-0000-0000-0000-000000000001"
    monkeypatch.setattr(
        adapter_server,
        "get_object",
        lambda *args, **kwargs: {
            "status": "ok",
            "object": {"guid": guid, "kind": "HTTPService", "name": "Api"},
        },
    )
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_forms",
        lambda payload: {"status": "ok", "forms": []},
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_parts",
        lambda payload: {
            "status": "ok",
            "source": {"kind": "live_metadata"},
            "object": {"guid": guid, "kind": "HTTPService", "name": "Api"},
            "parts": [
                {
                    "table": "Config",
                    "part_id": f"{guid}.0",
                    "suffix": ".0",
                    "classification": {
                        "role": "bsl_module_payload",
                        "stream_blocks": [
                            {"has_bsl_marker": True, "bytes": 512, "sha1": "short"},
                            {"has_bsl_marker": True, "bytes": 4096, "sha1": "full"},
                        ],
                    },
                }
            ],
            "counts": {"parts": 1},
        },
    )

    result = adapter_server.metadata_object_modules(
        {"base_id": "upo_test", "kind": "HTTPService", "name": "Api", "refresh_cache": True}
    )

    assert result["status"] == "ok"
    assert result["counts"]["modules"] == 1
    assert result["modules"][0]["kind"] == "http_service_module"
    assert result["modules"][0]["name"] == "Модуль HTTP-сервиса"
    assert result["modules"][0]["qualified_name"] == "Api.Модуль HTTP-сервиса"
    assert adapter_server.public_module_role(owner_kind="WebService")["kind"] == "web_service_module"
    assert adapter_server.public_module_role(owner_kind="IntegrationService")["kind"] == "integration_service_module"


def test_common_module_selects_only_largest_canonical_bsl_stream(monkeypatch: pytest.MonkeyPatch) -> None:
    guid = "10000000-0000-0000-0000-000000000009"
    monkeypatch.setattr(
        adapter_server,
        "get_object",
        lambda *args, **kwargs: {
            "status": "ok",
            "object": {"guid": guid, "kind": "CommonModule", "name": "ОбщийКод"},
        },
    )
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_parts",
        lambda payload: {
            "status": "ok",
            "source": {"kind": "live_metadata"},
            "object": {"guid": guid, "kind": "CommonModule", "name": "ОбщийКод"},
            "parts": [
                {
                    "table": "Config",
                    "part_id": f"{guid}.0",
                    "suffix": ".0",
                    "classification": {
                        "role": "bsl_module_payload",
                        "stream_blocks": [
                            {"has_bsl_marker": True, "bytes": 1024, "sha1": "fragment"},
                            {"has_bsl_marker": True, "bytes": 8192, "sha1": "canonical"},
                        ],
                    },
                }
            ],
            "counts": {"parts": 1},
        },
    )

    result = adapter_server.metadata_object_modules(
        {
            "base_id": "upo_test",
            "ref": "CommonModule.ОбщийКод",
            "refresh_cache": True,
            "include_storage": True,
        }
    )

    assert result["status"] == "ok"
    assert result["counts"]["modules"] == 1
    assert result["modules"][0]["kind"] == "common_module"
    assert result["modules"][0]["name"] == "ОбщийКод"
    assert result["modules"][0]["qualified_name"] == "ОбщийКод"
    assert result["modules"][0]["display_name"] == "ОбщийКод"
    assert result["modules"][0]["storage_name"] == f"{guid}.0#stream:1"
    assert result["modules"][0]["sha1"] == "canonical"


def test_configuration_parts_use_identity_guid_sql_prefix(monkeypatch: pytest.MonkeyPatch) -> None:
    root_guid = "10000000-0000-0000-0000-000000000001"
    identity_guid = "10000000-0000-0000-0000-000000000002"
    seen: dict[str, Any] = {}
    monkeypatch.setattr(
        adapter_server,
        "resolve_object_guid",
        lambda *args, **kwargs: (
            root_guid,
            "Configuration",
            {
                "guid": root_guid,
                "kind": "Configuration",
                "name": "Тестовая",
                "identity": {"guid": identity_guid, "name": "Тестовая"},
            },
            None,
        ),
    )

    def fake_files(payload: dict[str, Any]) -> dict[str, Any]:
        seen["prefix"] = payload["prefix"]
        return {"status": "ok", "files": [{"FileName": f"{identity_guid}.6"}]}

    monkeypatch.setattr(adapter_server, "storage_files_list", fake_files)
    monkeypatch.setattr(
        adapter_server,
        "read_storage_files_bytes",
        lambda *args, **kwargs: (
            {root_guid: b"root", f"{identity_guid}.6": b"module"},
            {"database": "upo_test"},
            None,
        ),
    )
    import parser.cas_payload as cas_payload

    monkeypatch.setattr(cas_payload, "classify_payload", lambda data, **kwargs: {"role": "metadata_payload", "stream_blocks": []})

    result = adapter_server.metadata_object_parts(
        {"base_id": "upo_test", "kind": "Configuration", "guid": root_guid, "include_storage": True}
    )

    assert seen["prefix"] == identity_guid
    assert [part["part_id"] for part in result["parts"]] == [root_guid, f"{identity_guid}.6"]
    assert result["parts"][1]["suffix"] == ".6"


def test_configuration_modules_include_empty_external_connection_module(monkeypatch: pytest.MonkeyPatch) -> None:
    root_guid = "10000000-0000-0000-0000-000000000001"
    identity_guid = "10000000-0000-0000-0000-000000000002"
    monkeypatch.setattr(
        adapter_server,
        "get_object",
        lambda *args, **kwargs: {
            "status": "ok",
            "object": {
                "guid": root_guid,
                "kind": "Configuration",
                "name": "Тестовая",
                "identity": {"guid": identity_guid, "name": "Тестовая"},
            },
        },
    )
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))

    def part(suffix: str, *, marker: bool) -> dict[str, Any]:
        streams = [{"has_bsl_marker": False, "bytes": 1} for _ in range(5)]
        streams[4] = {"has_bsl_marker": marker, "bytes": 100 if marker else 0, "sha1": suffix}
        return {
            "table": "Config",
            "part_id": f"{identity_guid}.{suffix}",
            "suffix": f".{suffix}",
            "classification": {"role": "bsl_module_payload" if marker else "stream_container", "stream_blocks": streams},
        }

    monkeypatch.setattr(
        adapter_server,
        "metadata_object_parts",
        lambda payload: {
            "status": "ok",
            "source": {"kind": "live_metadata"},
            "object": {"guid": root_guid, "kind": "Configuration", "name": "Тестовая"},
            "parts": [part("0", marker=True), part("5", marker=False), part("6", marker=True), part("7", marker=True)],
            "counts": {"parts": 4},
        },
    )

    result = adapter_server.metadata_object_modules(
        {"base_id": "upo_test", "kind": "Configuration", "name": "Тестовая", "refresh_cache": True}
    )

    assert [module["kind"] for module in result["modules"]] == [
        "ordinary_application_module",
        "external_connection_module",
        "managed_application_module",
        "session_module",
    ]
    assert all(module["module_ordinal"] >= 1 for module in result["modules"])


def test_metadata_object_properties_wraps_kind_specific_sql_decoder(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_special_details",
        lambda payload: {
            "schema": "onec_object_special_details.v1",
            "status": "ok",
            "base_id": payload["base_id"],
            "source": {"kind": "live_metadata"},
            "object": {"kind": "Configuration", "name": "Основная"},
            "details": {"vendor": "Test"},
            "counts": {"properties": 1},
        },
    )

    result = adapter_server.metadata_object_properties({"base_id": "upo_test", "ref": "Configuration.Основная"})

    assert result["schema"] == "onec_metadata_object_properties.v1"
    assert result["method"] == "metadata.object.properties"
    assert result["decoder"] == "kind_specific_sql"
    assert result["properties"] == {"vendor": "Test"}
    assert "details" not in result


def test_public_module_method_handler_returns_ready_routine_selector() -> None:
    handler = adapter_server.public_module_method_handler(
        "upo_test",
        {
            "guid": "00000000-0000-0000-0000-000000000001",
            "kind": "CommonModule",
            "name": "РегламентныеОперации",
            "ref": "CommonModule.РегламентныеОперации",
            "status": "ok",
        },
        "ВыполнитьЗадание",
    )

    assert handler["ref"] == "CommonModule.РегламентныеОперации.ВыполнитьЗадание"
    assert handler["read_selector"] == {
        "method": "modules.read",
        "base_id": "upo_test",
        "ref": "CommonModule.РегламентныеОперации",
        "routine_name": "ВыполнитьЗадание",
        "state": "working",
    }
    assert "read_selector" not in adapter_server.public_module_method_handler("upo_test", None, "ВыполнитьЗадание")


def test_public_pattern_value_type_preserves_union_references(monkeypatch: pytest.MonkeyPatch) -> None:
    first_guid = "00000000-0000-0000-0000-000000000101"
    second_guid = "00000000-0000-0000-0000-000000000102"
    type_node = {
        "type": "list",
        "items": [
            {"type": "string", "value": "Pattern"},
            {"type": "list", "items": [{"type": "string", "value": "#"}, {"type": "atom", "value": first_guid}]},
            {"type": "list", "items": [{"type": "string", "value": "#"}, {"type": "atom", "value": second_guid}]},
        ],
    }
    monkeypatch.setattr(
        adapter_server,
        "resolve_type_guids",
        lambda *args, **kwargs: {
            first_guid: {"status": "ok", "kind": "Catalog", "name": "Пользователи", "generated_category": "Ref"},
            second_guid: {"status": "ok", "kind": "Catalog", "name": "ВнешниеПользователи", "generated_category": "Ref"},
        },
    )

    value_type = adapter_server.public_pattern_value_type("upo_test", type_node)

    assert value_type["kind"] == "union"
    assert value_type["count"] == 2
    assert [item["presentation"] for item in value_type["types"]] == [
        "СправочникСсылка.Пользователи",
        "СправочникСсылка.ВнешниеПользователи",
    ]


def test_public_pattern_value_type_decodes_external_binary_data(monkeypatch: pytest.MonkeyPatch) -> None:
    type_node = {
        "type": "list",
        "items": [
            {"type": "string", "value": "Pattern"},
            {"type": "list", "items": [{"type": "string", "value": "R"}]},
        ],
    }
    monkeypatch.setattr(adapter_server, "resolve_type_guids", lambda *args, **kwargs: {})

    value_type = adapter_server.public_pattern_value_type("upo_test", type_node)

    assert value_type == {"kind": "binary", "presentation": "ДвоичныеДанные"}


def test_builtin_session_parameter_type_overrides_stale_unresolved_cache(monkeypatch: pytest.MonkeyPatch) -> None:
    type_guid = "220455ea-6c85-4513-996f-bbe79ed07774"
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"server": "test"}, None))
    monkeypatch.setattr(
        adapter_server,
        "metadata_guid_index_lookup_types",
        lambda config, guids: {type_guid: {"guid": type_guid, "status": "generated_type_unresolved"}},
    )
    monkeypatch.setattr(adapter_server, "metadata_type_cache_lookup_many", lambda config, guids: {})
    monkeypatch.setattr(adapter_server, "metadata_type_cache_upsert", lambda *args, **kwargs: None)

    resolved = adapter_server.resolve_type_guids("upo_test", {type_guid})

    assert resolved[type_guid]["status"] == "ok"
    assert resolved[type_guid]["presentation"] == "ФиксированноеСоответствие"
    assert resolved[type_guid]["bsl_type"] == "FixedMap"


def test_settings_storage_suffix_zero_is_public_manager_module() -> None:
    role = adapter_server.public_module_role(owner_kind="SettingsStorage", suffix="8")

    assert role == {"kind": "manager_module", "name": "Модуль менеджера", "suffix": "8"}


def test_standard_common_command_groups_have_public_names() -> None:
    assert adapter_server.STANDARD_COMMAND_GROUP_GUIDS["77ea1b8f-dd79-4717-9dba-5628e7f348cf"] == "NavigationPanelOrdinary"
    assert adapter_server.STANDARD_COMMAND_GROUP_GUIDS["aabb34e1-98c1-4bd0-bf7f-243f95437b44"] == "ActionsPanelTools"


def test_sql_only_decoders_cover_event_subscription_and_services(monkeypatch: pytest.MonkeyPatch) -> None:
    atom = lambda value: {"type": "atom", "value": str(value)}
    string = lambda value: {"type": "string", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}

    def identity(guid: str, name: str, synonym: str) -> dict[str, Any]:
        return seq(atom(3), seq(atom(1), atom(0), atom(guid)), string(name), seq(atom(1), string("ru"), string(synonym)), string(""), atom(0), atom(0), atom("00000000-0000-0000-0000-000000000000"), atom(0))

    monkeypatch.setattr(
        adapter_server,
        "public_metadata_guid_reference",
        lambda base_id, guid: {"guid": guid, "kind": "Document", "name": "Заказ", "ref": "Document.Заказ", "status": "ok"},
    )
    subscription_tree = seq(
        atom(1),
        seq(
            atom(1),
            identity("00000000-0000-0000-0000-000000000001", "Подписка", "Подписка"),
            seq(atom("Pattern"), seq(atom("#"), atom("00000000-0000-0000-0000-000000000002"))),
            atom("BeforeWrite_ПередЗаписью"),
            atom("00000000-0000-0000-0000-000000000003"),
            string("Обработать"),
        ),
        atom(0),
    )

    subscription = adapter_server.event_subscription_sql_details("upo_test", subscription_tree)

    assert subscription["event"] == "BeforeWrite"
    assert subscription["event_code"] == "BeforeWrite_ПередЗаписью"
    assert subscription["sources"][0]["ref"] == "Document.Заказ"
    assert subscription["handler"]["method"] == "Обработать"
    assert subscription["handler"]["read_selector"] == {
        "method": "modules.read",
        "base_id": "upo_test",
        "ref": "Document.Заказ",
        "routine_name": "Обработать",
        "state": "working",
    }

    operation_identity = identity("00000000-0000-0000-0000-000000000004", "execute", "execute")
    parameter_identity = identity("00000000-0000-0000-0000-000000000005", "request", "request")
    parameter = seq(atom(0), parameter_identity, seq(atom(0), string("urn:test"), string("Request")), atom(0), atom(0))
    operation = seq(atom(1), operation_identity, seq(atom(0), string("urn:test"), string("Response")), atom(0), atom(0), string("Execute"), atom(1))
    web_tree = seq(
        atom(1),
        seq(
            atom(4), string("urn:test"), identity("00000000-0000-0000-0000-000000000006", "Service", "Service"),
            seq(atom(0), atom(1), seq(atom("#"), atom("00000000-0000-0000-0000-000000000007"), seq(atom(1), atom("00000000-0000-0000-0000-000000000008")))),
            string("service.1cws"), seq(atom(0)), atom(0), atom(20),
        ),
        atom(1),
        seq(atom("collection"), atom(1), seq(operation, atom(1), seq(atom("parameters"), atom(1), seq(parameter, atom(0))))),
    )

    web = adapter_server.web_service_sql_details("upo_test", web_tree)

    assert web["namespace"] == "urn:test"
    assert web["descriptor_file_name"] == "service.1cws"
    assert web["reuse_sessions"] == "DontUse"
    assert web["operations"][0]["procedure_name"] == "Execute"
    assert web["operations"][0]["data_lock_control_mode"] == "Managed"
    assert web["operations"][0]["parameters"][0]["transfer_direction"] == "In"

    method_identity = identity("00000000-0000-0000-0000-000000000009", "POST", "POST")
    template_identity = identity("00000000-0000-0000-0000-000000000010", "Notify", "Notify")
    method = seq(atom(0), string("NotifyPOST"), atom(11), method_identity)
    template = seq(atom(0), string("/notify"), template_identity)
    http_tree = seq(
        atom(1),
        seq(atom(2), string("api"), identity("00000000-0000-0000-0000-000000000011", "Http", "Http"), atom(2), atom(20)),
        atom(1),
        seq(atom("collection"), atom(1), seq(template, atom(1), seq(atom("methods"), atom(1), seq(method)))),
    )

    http = adapter_server.http_service_sql_details(http_tree)

    assert http["root_url"] == "api"
    assert http["reuse_sessions"] == "AutoUse"
    assert http["url_templates"][0]["template"] == "/notify"
    assert http["url_templates"][0]["methods"][0]["http_method"] == "POST"
    assert http["url_templates"][0]["methods"][0]["handler"] == "NotifyPOST"


def test_external_data_source_sql_decoder_reads_tables_fields_types_and_keys(monkeypatch: pytest.MonkeyPatch) -> None:
    atom = lambda value: {"type": "atom", "value": str(value)}
    string = lambda value: {"type": "string", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    source_guid = "10000000-0000-0000-0000-000000000001"
    table_guid = "10000000-0000-0000-0000-000000000002"
    field_guid = "10000000-0000-0000-0000-000000000003"

    def identity(guid: str, name: str, synonym: str) -> dict[str, Any]:
        return seq(atom(1), seq(atom(0), atom(0), atom(guid)), string(name), seq(atom(1), string("ru"), string(synonym)), string(""), atom(0), atom(0))

    source_tree = seq(
        atom(1),
        seq(atom(2), seq(atom(0), identity(source_guid, "Источник", "Источник")), atom(0), atom(0), atom(0), atom(0), atom(0), atom(0), atom(0), atom(0)),
        atom(3),
        seq(atom("cubes"), atom(0)),
        seq(atom("functions"), atom(0)),
        seq(atom("tables"), atom(1), atom(table_guid)),
    )
    header = [atom(0) for _ in range(19)]
    header[0] = atom(11)
    header[1] = seq(atom(0), identity(table_guid, "Продажи", "Продажи"))
    header[16] = atom(0)
    header[17] = string("dbo.SALES")
    header[18] = seq(atom(0), atom(1), seq(string("#"), atom("157fa490-4ce9-11d4-9415-008048da11f9"), seq(atom(1), atom(field_guid))))
    definition = seq(atom(2), identity(field_guid, "Код", "Код"), seq(string("Pattern"), seq(string("N"), atom(10), atom(0), atom(0))))
    field_properties = seq(atom(27), definition)
    field = seq(seq(atom(1), field_properties, string("CODE"), atom(1), atom(0)), atom(0))
    table_tree = seq(
        atom(1),
        seq(*header),
        atom(4),
        seq(atom(0), atom(0)),
        seq(atom(0), atom(0)),
        seq(atom(0), atom(0)),
        seq(atom("fields"), atom(1), field),
    )
    monkeypatch.setattr(adapter_server, "read_storage_files_bytes", lambda *args, **kwargs: ({table_guid: b"table"}, {}, None))
    monkeypatch.setattr(adapter_server, "parse_config_tree_from_bytes", lambda data: table_tree)
    monkeypatch.setattr(adapter_server, "resolve_type_guids", lambda *args, **kwargs: {})

    decoded = adapter_server.external_data_source_sql_details("upo_test", source_tree)

    assert decoded["counts"] == {
        "tables": 1,
        "declared_tables": 1,
        "fields": 1,
        "typed_fields": 1,
        "cubes": 0,
        "declared_cubes": 0,
        "functions": 0,
        "declared_functions": 0,
        "missing_child_payloads": 0,
    }
    table = decoded["tables"][0]
    assert table["ref"] == "ExternalDataSource.Источник.Table.Продажи"
    assert table["name_in_data_source"] == "dbo.SALES"
    assert table["key_fields"][0]["identity"]["name"] == "Код"
    assert table["fields"][0]["value_type"]["presentation"] == "Число(10, 0)"
    assert table["fields"][0]["ref"] == "ExternalDataSource.Источник.Table.Продажи.Field.Код"


def test_templates_read_resolves_top_level_common_template_by_public_ref(monkeypatch: pytest.MonkeyPatch) -> None:
    guid = "10000000-0000-0000-0000-000000000001"
    monkeypatch.setattr(
        adapter_server,
        "resolve_object_guid",
        lambda *args, **kwargs: (guid, "CommonTemplate", {"guid": guid, "kind": "CommonTemplate", "name": "Печать"}, None),
    )
    monkeypatch.setattr(
        adapter_server,
        "read_template_by_guid",
        lambda payload, analyze=False: {
            "schema": "onec_templates_analyze.v1" if analyze else "onec_templates_read.v1",
            "status": "ok",
            "object": {"guid": payload["guid"], "kind": "Template"},
            "templates": [{"guid": payload["guid"], "name": "Печать"}],
            "counts": {"templates": 1, "parts": 2},
        },
    )

    result = adapter_server.templates_read({"base_id": "upo_test", "ref": "CommonTemplate.Печать", "view": "summary"})

    assert result["status"] == "ok"
    assert result["counts"] == {"templates": 1, "parts": 2}
    assert result["templates"][0]["name"] == "Печать"


def test_defined_type_selection_criterion_and_enum_sql_decoders(monkeypatch: pytest.MonkeyPatch) -> None:
    atom = lambda value: {"type": "atom", "value": str(value)}
    string = lambda value: {"type": "string", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}

    def identity(guid: str, name: str, synonym: str, comment: str = "") -> dict[str, Any]:
        return seq(atom(3), seq(atom(1), atom(0), atom(guid)), string(name), seq(atom(1), string("ru"), string(synonym)), string(comment), atom(0), atom(0), atom("00000000-0000-0000-0000-000000000000"), atom(0))

    type_guid = "10000000-0000-0000-0000-000000000010"
    content_guid = "10000000-0000-0000-0000-000000000020"
    monkeypatch.setattr(
        adapter_server,
        "public_pattern_value_type",
        lambda *args, **kwargs: {
            "kind": "union",
            "presentation": "СправочникСсылка.А | ДокументСсылка.Б",
            "types": [
                {"kind": "reference", "presentation": "СправочникСсылка.А"},
                {"kind": "reference", "presentation": "ДокументСсылка.Б"},
            ],
            "count": 2,
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "public_metadata_guid_references",
        lambda *args, **kwargs: {
            content_guid: {
                "guid": content_guid,
                "kind": "Attribute",
                "name": "Заказ",
                "ref": "Document.Оплата.Attribute.Заказ",
                "status": "ok",
            }
        },
    )

    defined_tree = seq(
        atom(1),
        seq(atom(0), atom(type_guid), atom(type_guid), identity("10000000-0000-0000-0000-000000000001", "СоставнойТип", "Составной тип", "Комментарий"), seq(string("Pattern"), seq(string("#"), atom(type_guid)))),
        atom(0),
    )
    defined = adapter_server.defined_type_sql_details("upo_test", defined_tree)
    assert defined["value_type"]["kind"] == "union"
    assert defined["counts"] == {"types": 2, "resolved_types": 2}

    criterion_tree = seq(
        atom(1),
        seq(
            atom(14), atom(0), atom(0), atom(0), atom(0),
            seq(atom(2), identity("10000000-0000-0000-0000-000000000002", "ПоЗаказу", "По заказу"), seq(string("Pattern"), seq(string("#"), atom(type_guid)))),
            seq(atom(0), atom(1), seq(string("#"), atom("157fa490-4ce9-11d4-9415-008048da11f9"), seq(atom(1), atom(content_guid)))),
            atom(0), atom("00000000-0000-0000-0000-000000000000"), atom("00000000-0000-0000-0000-000000000000"), seq(atom(0)), seq(atom(0)), seq(atom(1), string("ru"), string("Документы по заказу")),
        ),
        atom(2), seq(atom(0), atom(0)), seq(atom(0), atom(0)),
    )
    criterion = adapter_server.selection_criterion_sql_details("upo_test", criterion_tree)
    assert criterion["use_standard_commands"] is False
    assert criterion["list_presentation"] == {"ru": "Документы по заказу"}
    assert criterion["content"][0]["ref"] == "Document.Оплата.Attribute.Заказ"
    assert criterion["counts"]["resolved_content"] == 1

    enum_name = "Состояния"
    first_value = seq(seq(atom(0), identity("10000000-0000-0000-0000-000000000003", "Новый", "Новый")), atom(0))
    second_value = seq(seq(atom(0), identity("10000000-0000-0000-0000-000000000004", "Закрыт", "Закрыт")), atom(0))
    enum_body = [atom(0) for _ in range(21)]
    enum_body[5] = seq(atom(0), identity("10000000-0000-0000-0000-000000000005", enum_name, enum_name))
    enum_body[6] = atom(0)
    enum_body[11] = atom(2)
    enum_body[12] = atom(1)
    enum_tree = seq(atom(1), seq(*enum_body), atom(4), seq(atom(0), atom(0)), seq(atom(0), atom(0)), seq(atom(0), atom(0)), seq(atom("values"), atom(2), first_value, second_value))
    enum = adapter_server.enum_sql_details(enum_tree)
    assert enum["quick_choice"] is True
    assert enum["choice_mode"] == "BothWays"
    assert [item["ref"] for item in enum["values"]] == ["Enum.Состояния.Value.Новый", "Enum.Состояния.Value.Закрыт"]


def test_generated_type_decoder_finds_late_manager_pair() -> None:
    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    guids = [f"00000000-0000-0000-0000-{index:012d}" for index in range(1, 13)]
    tree = seq(
        atom(1),
        seq(
            atom(40),
            atom(guids[0]), atom(guids[1]), atom(guids[2]), atom(guids[3]),
            atom(guids[4]), atom(guids[5]), atom(guids[6]), atom(guids[7]),
            seq(atom(0)), atom(1), atom(2), atom(3),
            atom(guids[8]), atom(guids[9]),
            atom(0), atom(0),
            atom(guids[10]), atom(guids[11]),
        ),
    )

    records = adapter_server.generated_type_records_from_tree(
        tree,
        kind="Document",
        name="Заказ",
        guid="10000000-0000-0000-0000-000000000001",
    )

    assert [record["category"] for record in records] == ["Object", "Ref", "Selection", "List", "Manager"]
    assert records[-1]["type_guid"] == guids[10]
    assert records[-1]["presentation"] == "cfg:DocumentManager.Заказ"


def test_generated_type_decoder_skips_identity_and_maps_constant_and_business_process_pairs() -> None:
    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    identity = seq(atom(3), seq(atom(1), atom(0), atom("10000000-0000-0000-0000-000000000001")), atom("Объект"), seq(atom(0)))
    guids = [f"20000000-0000-0000-0000-{index:012d}" for index in range(1, 17)]
    constant_tree = seq(
        atom(1),
        seq(atom(16), identity, atom(guids[0]), atom(guids[1]), atom(guids[2]), atom(guids[3]), atom(1), atom(0), seq(atom(0)), atom(0), atom(guids[4]), atom(guids[5])),
    )
    constant_records = adapter_server.generated_type_records_from_tree(
        constant_tree,
        kind="Constant",
        name="Флаг",
        guid="10000000-0000-0000-0000-000000000002",
    )
    assert [record["category"] for record in constant_records[:2]] == ["Manager", "ValueManager"]
    assert constant_records[1]["type_guid"] == guids[2]

    business_process_tree = seq(
        atom(1),
        seq(
            atom(30), identity, atom(1),
            *[atom(value) for value in guids[:12]],
            seq(atom(0)), atom(1), atom(0), atom(11), atom(1), atom(1), atom(1), atom(guids[12]), atom(guids[13]),
        ),
    )
    business_records = adapter_server.generated_type_records_from_tree(
        business_process_tree,
        kind="BusinessProcess",
        name="Заявка",
        guid="10000000-0000-0000-0000-000000000003",
    )
    assert [record["category"] for record in business_records] == [
        "Object", "Ref", "Selection", "List", "RoutePointRef", "RoutePoint", "Manager"
    ]
    assert business_records[4]["name"] == "BusinessProcessRoutePointRef.Заявка"


def test_role_rights_decoder_covers_flags_rights_rls_and_templates(monkeypatch: pytest.MonkeyPatch) -> None:
    atom = lambda value: {"type": "atom", "value": str(value)}
    string = lambda value: {"type": "string", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    object_guid = "10000000-0000-0000-0000-000000000001"
    read_guid = "1c87578f-9e09-4ec0-a991-5629c87b1588"
    update_guid = "287b74b8-3a66-4a76-ba27-4f1f6a93770e"
    restriction = seq(read_guid, seq(atom(1), seq(atom(1), string("ГДЕ ИСТИНА"), atom(0))))
    rights = seq(atom(1), atom(2), atom(read_guid), atom(1), atom(update_guid), atom(-1), atom(1), restriction)
    record = seq(seq(atom(1), atom(object_guid), atom(0), atom(0)), rights)
    tree = seq(
        atom(10),
        seq(atom(1), record),
        seq(atom(1), seq(string("ДляОбъекта"), string("ГДЕ Ссылка = &Ссылка"))),
        atom(1), atom(1), atom(0), atom("4294967295"),
    )
    monkeypatch.setattr(
        adapter_server,
        "public_metadata_guid_references",
        lambda *args, **kwargs: {
            object_guid: {
                "guid": object_guid,
                "kind": "Document",
                "name": "Заказ",
                "ref": "Document.Заказ",
                "status": "ok",
            }
        },
    )

    decoded = adapter_server.role_rights_sql_details("upo_test", tree)

    assert decoded["set_for_new_objects"] is True
    assert decoded["set_for_attributes_by_default"] is True
    assert decoded["independent_rights_of_child_objects"] is False
    assert decoded["objects"][0]["object"]["ref"] == "Document.Заказ"
    assert decoded["objects"][0]["rights"][0]["name"] == "Read"
    assert decoded["objects"][0]["rights"][0]["state"] == "allowed"
    assert decoded["objects"][0]["rights"][0]["restrictions"][0]["condition"] == "ГДЕ ИСТИНА"
    assert decoded["objects"][0]["rights"][1]["name"] == "Update"
    assert decoded["objects"][0]["rights"][1]["state"] == "denied"
    assert decoded["restriction_templates"][0]["name"] == "ДляОбъекта"
    assert decoded["counts"]["restrictions"] == 1


def test_nested_command_identity_and_category_are_decoded_from_config_tree() -> None:
    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    command_guid = "10000000-0000-0000-0000-000000000001"
    command_class = "078a6af8-d22c-4248-9c33-7e90075a3d2c"
    identity = seq(
        seq(atom(1), atom(0), atom(command_guid)),
        atom("Выполнить"),
        seq(atom(1), atom("ru"), atom("Выполнить действие")),
    )
    tree = seq(seq(atom(2), atom(command_guid), atom(command_class)), identity)

    identities = adapter_server.config_tree_identity_records(tree)
    categories = adapter_server.config_tree_nested_categories(tree, {command_guid})

    assert identities[command_guid]["name"] == "Выполнить"
    assert identities[command_guid]["synonyms"] == {"ru": "Выполнить действие"}
    assert categories == {command_guid: "Command"}


def test_nested_identity_index_accepts_multilingual_and_empty_synonyms() -> None:
    atom = lambda value: {"type": "atom", "value": str(value)}
    string = lambda value: {"type": "string", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    multilingual_guid = "10000000-0000-0000-0000-000000000001"
    empty_guid = "10000000-0000-0000-0000-000000000002"
    tree = seq(
        seq(atom(1), atom(0), atom(multilingual_guid)),
        string("Многоязычный"),
        seq(atom(2), string("ru"), string("Русский"), string("en"), string("English")),
        seq(
            seq(atom(1), atom(0), atom(empty_guid)),
            string("БезСинонима"),
            seq(atom(0)),
        ),
    )

    identities = adapter_server.config_tree_identity_records(tree)

    assert identities[multilingual_guid]["synonyms"] == {"ru": "Русский", "en": "English"}
    assert identities[empty_guid]["name"] == "БезСинонима"
    assert identities[empty_guid]["synonyms"] == {}


def test_config_identity_accepts_multilingual_and_empty_synonyms() -> None:
    atom = lambda value: {"type": "atom", "value": str(value)}
    string = lambda value: {"type": "string", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    empty_guid = "10000000-0000-0000-0000-000000000002"
    multilingual_guid = "10000000-0000-0000-0000-000000000003"

    empty = find_identity(seq(seq(atom(1), atom(0), atom(empty_guid)), string("БезСинонима"), seq(atom(0))))
    multilingual = find_identity(
        seq(
            seq(atom(1), atom(0), atom(multilingual_guid)),
            string("Многоязычный"),
            seq(atom(2), string("ru"), string("Русский"), string("en"), string("English")),
        )
    )

    assert empty is not None
    assert empty.name == "БезСинонима"
    assert empty.synonyms == {}
    assert multilingual is not None
    assert multilingual.synonyms == {"ru": "Русский", "en": "English"}


def test_integration_channel_reference_maps_dbnames_order_to_sql_config_channels(monkeypatch: pytest.MonkeyPatch) -> None:
    from types import SimpleNamespace

    service_guid = "10000000-0000-0000-0000-000000000001"
    first_guid = "10000000-0000-0000-0000-000000000002"
    second_guid = "10000000-0000-0000-0000-000000000003"
    records = [
        SimpleNamespace(guid=service_guid, storage_role="IntegServiceSettings", sql_number=10, source="DBNames", index=1),
        SimpleNamespace(guid=first_guid, storage_role="IntegChannelInQueue", sql_number=11, source="DBNames", index=2),
        SimpleNamespace(guid=second_guid, storage_role="IntegChannelInQueue", sql_number=12, source="DBNames", index=3),
    ]
    monkeypatch.setattr(adapter_server, "read_storage_files_bytes", lambda *args, **kwargs: ({service_guid: b"payload"}, {}, None))
    monkeypatch.setattr(adapter_server, "parse_config_tree_from_bytes", lambda data: {})
    monkeypatch.setattr(adapter_server, "config_identity_from_bytes", lambda data: {"guid": service_guid, "name": "Обмен"})
    monkeypatch.setattr(
        adapter_server,
        "integration_service_sql_details",
        lambda tree, include_storage=False: {
            "channels": [
                {"name": "ВходОбычный", "message_direction": "Receive"},
                {"name": "ВходСрочный", "message_direction": "Receive"},
            ]
        },
    )
    monkeypatch.setattr(adapter_server, "cache_nested_metadata_reference", lambda *args, **kwargs: None)

    resolved = adapter_server.integration_channel_guid_references(
        "upo_test",
        [first_guid, second_guid],
        dbnames_records=records,
    )

    assert resolved[first_guid]["ref"] == "IntegrationService.Обмен.Channel.ВходОбычный"
    assert resolved[second_guid]["name"] == "ВходСрочный"
    assert resolved[second_guid]["owner"]["ref"] == "IntegrationService.Обмен"


def test_metadata_object_properties_falls_back_to_generic_sql_semantics(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "metadata_object_special_details", lambda payload: {"status": "unsupported"})
    monkeypatch.setattr(
        adapter_server,
        "get_object",
        lambda *args, **kwargs: {
            "schema": "onec_metadata_object.v1",
            "status": "ok",
            "base_id": kwargs["base_id"],
            "source": {"kind": "live_metadata"},
            "object": {"kind": "Document", "name": "Заказ"},
            "semantic": {"sections": [{"kind": "attributes"}]},
            "counts": {"matches": 1},
        },
    )

    result = adapter_server.metadata_object_properties({"base_id": "upo_test", "kind": "Document", "name": "Заказ"})

    assert result["status"] == "ok"
    assert result["decoder"] == "generic_semantic_sql"
    assert result["properties"] == {"sections": [{"kind": "attributes"}]}


def test_metadata_object_properties_is_public_and_rejects_xml_runtime_source() -> None:
    help_result = call_method("help.methods", {"method": "metadata.object.properties"})
    blocked = call_method(
        "metadata.object.properties",
        {"base_id": "upo_test", "kind": "Document", "name": "X", "xml_path": "Z:\\offline.xml"},
    )

    assert help_result["count"] == 1
    assert blocked["status"] == "invalid_argument"
    assert blocked["argument"] == "payload.xml_path"


def test_form_owner_cache_resolves_common_form_by_name(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    config = {"server": "sql.example", "database": "upo_test"}

    adapter_server.metadata_form_owner_cache_upsert(
        config,
        base_id="upo_test",
        extension={"guid": "fb26cf42-7609-11f1-828f-005056b0d483", "name": "test2"},
        owner_kind="CommonForm",
        owner_name="t_Форма",
        form_name="t_Форма",
        table="ConfigCAS",
        file_name="adb1ca009585150335d928b709e79a3c52fdb3a2",
        form_guid="adb1ca009585150335d928b709e79a3c52fdb3a2",
        bsl_offset=1375,
    )

    cached = adapter_server.metadata_form_owner_cache_lookup(
        config,
        owner_kind="CommonForm",
        form_name="t_Форма",
        extension="test2",
    )

    assert cached is not None
    assert cached["form"]["name"] == "t_Форма"
    assert cached["source"] == {"table": "ConfigCAS", "file_name": "adb1ca009585150335d928b709e79a3c52fdb3a2"}
    assert cached["module_ref"] == "ConfigCAS:adb1ca009585150335d928b709e79a3c52fdb3a2"
    assert cached["bsl_offset"] == 1375


def test_form_owner_cache_resolves_form_module_ref(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    config = {"server": "sql.example", "database": "upo_test"}
    file_name = "adb1ca009585150335d928b709e79a3c52fdb3a2"

    adapter_server.metadata_form_owner_cache_upsert(
        config,
        base_id="upo_test",
        extension={"name": "test2"},
        owner_kind="CommonForm",
        form_name="t_Форма",
        table="ConfigCAS",
        file_name=file_name,
        bsl_offset=1375,
    )

    cached = adapter_server.metadata_form_owner_cache_lookup(
        config,
        module_ref=f"ConfigCAS:{file_name}#form_module",
    )

    assert cached is not None
    assert cached["owner"]["kind"] == "CommonForm"
    assert cached["form"]["name"] == "t_Форма"
    assert cached["bsl_offset"] == 1375


def test_module_owner_cache_prune_resolves_name_and_supports_dry_run(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    config = {"server": "sql.example", "database": "upo_test"}
    owner_guid = "11111111-1111-1111-1111-111111111111"
    other_owner_guid = "22222222-2222-2222-2222-222222222222"
    first_ref = "Config:first.0#stream:1"
    second_ref = "Config:second.0#stream:2"
    other_ref = "Config:other.0#stream:3"

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_modules_owner_guid_from_selector",
        lambda base_id, payload, timeout_seconds: (
            owner_guid,
            {
                "status": "resolved",
                "method": "metadata.objects.list",
                "owner_guid": owner_guid,
                "object": {
                    "kind": "CommonModule",
                    "name": "ОбщегоНазначения",
                    "guid": owner_guid,
                    "ref": "CommonModule.ОбщегоНазначения",
                },
            },
        ),
    )
    adapter_server.metadata_module_owner_cache_upsert(
        config,
        first_ref,
        {"kind": "CommonModule", "name": "ОбщегоНазначения", "guid": owner_guid},
    )
    adapter_server.metadata_module_owner_cache_upsert(
        config,
        second_ref,
        {"kind": "CommonModule", "name": "ОбщегоНазначения", "guid": owner_guid},
    )
    adapter_server.metadata_module_owner_cache_upsert(
        config,
        other_ref,
        {"kind": "CommonModule", "name": "ДругойМодуль", "guid": other_owner_guid},
    )

    dry_run = adapter_server.metadata_module_owner_cache_prune(
        {
            "base_id": "upo_test",
            "object_type": "CommonModule",
            "object_name": "ОбщегоНазначения",
            "dry_run": True,
        }
    )

    assert dry_run["status"] == "ok"
    assert dry_run["dry_run"] is True
    assert dry_run["counts"] == {"matched": 2}
    assert dry_run["resolved_by"] == "public_object_selector"
    assert dry_run["query"]["owner"]["ref"] == "CommonModule.ОбщегоНазначения"
    assert "guid" not in dry_run["query"]["owner"]
    assert "owner_guid" not in dry_run["query"]
    assert adapter_server.metadata_module_owner_cache_lookup(config, first_ref) is not None

    applied = adapter_server.metadata_module_owner_cache_prune(
        {
            "base_id": "upo_test",
            "ref": "CommonModule.ОбщегоНазначения",
            "dry_run": False,
        }
    )

    assert applied["counts"] == {"matched": 2, "deleted": 2}
    assert adapter_server.metadata_module_owner_cache_lookup(config, first_ref) is None
    assert adapter_server.metadata_module_owner_cache_lookup(config, second_ref) is None
    assert adapter_server.metadata_module_owner_cache_lookup(config, other_ref) is not None


def test_module_owner_cache_prune_rejects_unresolved_name(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_modules_owner_guid_from_selector",
        lambda base_id, payload, timeout_seconds: (
            None,
            {
                "status": "not_found",
                "selector": {"object_type": "CommonModule", "object_name": "НетТакого"},
            },
        ),
    )

    result = adapter_server.metadata_module_owner_cache_prune(
        {
            "base_id": "upo_test",
            "ref": "CommonModule.НетТакого",
            "dry_run": True,
        }
    )

    assert result["status"] == "not_found"
    assert result["error"] == "owner_not_resolved"
    assert result["selector"] == {
        "ref": "CommonModule.НетТакого",
        "kind": "CommonModule",
        "name": "НетТакого",
    }


def test_module_owner_cache_upsert_enriches_active_and_saved_code_index_rows(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    config = {"server": "sql.example", "database": "upo_test"}
    active_ref = "Config:common-module.0#stream:4"
    saved_ref = "ConfigSave:common-module.0#stream:4"
    text = "Процедура Проверка()\nКонецПроцедуры"
    for table, module_ref in (("Config", active_ref), ("ConfigSave", saved_ref)):
        adapter_server.code_index_upsert(
            config,
            base_id="upo_test",
            table=table,
            file_name="common-module.0",
            module_ref=module_ref,
            data=text.encode("utf-8"),
            text=text,
        )

    enriched = adapter_server.metadata_module_owner_cache_upsert(
        config,
        active_ref,
        {
            "kind": "CommonModule",
            "name": "ОбщегоНазначения",
            "guid": "11111111-1111-1111-1111-111111111111",
        },
    )

    assert enriched == 2
    assert adapter_server.metadata_module_owner_cache_lookup(config, saved_ref)["owner"]["name"] == "ОбщегоНазначения"
    with adapter_server.cache_connection() as conn:
        rows = conn.execute(
            "SELECT owner_kind, owner_name, owner_guid FROM metadata_code_index_cache ORDER BY module_ref"
        ).fetchall()
    assert len(rows) == 2
    assert {row["owner_name"] for row in rows} == {"ОбщегоНазначения"}


def test_module_owner_cache_backfill_returns_resumable_name_first_cursor(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    config = {"server": "sql.example", "database": "upo_test"}
    owner_guid = "11111111-1111-1111-1111-111111111111"
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    monkeypatch.setattr(
        adapter_server,
        "list_objects",
        lambda *args, **kwargs: {
            "status": "ok",
            "objects": [{"kind": "CommonModule", "name": "ОбщегоНазначения", "guid": owner_guid}],
            "counts": {"total": 2, "total_visible": 2},
        },
    )

    def fake_modules(payload: dict[str, Any]) -> dict[str, Any]:
        module_ref = "Config:common-module.0#stream:4"
        adapter_server.metadata_module_owner_cache_upsert(
            config,
            module_ref,
            {"kind": "CommonModule", "name": "ОбщегоНазначения", "guid": owner_guid},
        )
        return {
            "status": "ok",
            "object": {"kind": "CommonModule", "name": "ОбщегоНазначения", "guid": owner_guid},
            "modules": [{"module_id": module_ref}],
        }

    monkeypatch.setattr(adapter_server, "metadata_object_modules", fake_modules)

    result = adapter_server.metadata_module_owner_cache_backfill(
        {"base_id": "upo_test", "kind": "CommonModule", "limit": 1, "offset": 0, "deep": True}
    )

    assert result["status"] == "ok"
    assert result["complete"] is False
    assert result["next_cursor"] == {
        "kind_index": adapter_server.metadata_module_owner_backfill_kinds().index("CommonModule"),
        "kind": "CommonModule",
        "offset": 1,
    }
    assert result["objects"] == [
        {
            "ref": "CommonModule.ОбщегоНазначения",
            "modules": 1,
            "resolved_by": "metadata.object.modules",
        }
    ]
    assert result["counts"]["cache_entries_added"] == 1


def test_module_owner_resolve_from_ref_verifies_embedded_guid(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    config = {"server": "sql.example", "database": "upo_test"}
    owner_guid = "05a138aa-b3c0-48c6-94ea-0acd4fc000cf"
    module_ref = f"Config:{owner_guid}.0#stream:4"
    calls: list[tuple[str | None, str]] = []

    def fake_get_object(kind: str | None, name: str, **kwargs: Any) -> dict[str, Any]:
        calls.append((kind, name))
        return {
            "status": "ok",
            "object": {
                "kind": "CommonModule",
                "name": "ИнтеграцияС1СДокументооборотКлиентСервер",
                "synonym": "Интеграция с 1С:Документооборот",
                "guid": owner_guid,
            },
        }

    monkeypatch.setattr(adapter_server, "get_object", fake_get_object)

    resolved = adapter_server.metadata_module_owner_resolve_from_ref(
        "upo_test",
        config,
        module_ref,
    )

    assert calls == [(None, owner_guid)]
    assert resolved["owner"]["kind"] == "CommonModule"
    assert resolved["owner"]["name"] == "ИнтеграцияС1СДокументооборотКлиентСервер"
    assert adapter_server.metadata_module_owner_cache_lookup(config, module_ref)["owner"]["guid"] == owner_guid


def test_extension_module_owner_resolve_uses_current_manifest_and_enriches_code_index(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    config = {"server": "sql.example", "database": "upo_test"}
    extension_guid = "7ba4e0fe-98c8-11ed-9fb6-0050569db20c"
    owner_guid = "11111111-1111-1111-1111-111111111111"
    descriptor_key = "a" * 40
    module_key = "b" * 40
    module_ref = f"ConfigCAS:{module_key}#stream:3"
    text = "Функция ИзРасширения()\nКонецФункции"
    adapter_server.code_index_upsert(
        config,
        base_id="upo_test",
        table="ConfigCAS",
        file_name=module_key,
        module_ref=module_ref,
        data=text.encode("utf-8"),
        text=text,
    )
    related_entries = [
        {
            "object_base_id": owner_guid,
            "suffix": "",
            "cas_key": descriptor_key,
            "extension": {"guid": extension_guid, "name": "EF_UPO_SODECSO"},
            "root_cas_key": "c" * 40,
        },
        {
            "object_base_id": owner_guid,
            "suffix": ".0",
            "cas_key": module_key,
            "extension": {"guid": extension_guid, "name": "EF_UPO_SODECSO"},
            "root_cas_key": "c" * 40,
        },
    ]
    monkeypatch.setattr(
        adapter_server,
        "manifest_related_entries_for_cas_key",
        lambda *args, **kwargs: (related_entries, []),
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda *args, **kwargs: (b"descriptor", config, None),
    )
    monkeypatch.setattr(
        adapter_server,
        "config_identity_from_bytes",
        lambda data: {
            "guid": owner_guid,
            "name": "РасширенныйОбщийМодуль",
            "synonyms": {"ru": "Расширенный общий модуль"},
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "extension_metadata_payload_kind",
        lambda *args, **kwargs: "CommonModule",
    )

    resolved = adapter_server.metadata_module_owner_resolve_from_ref(
        "upo_test",
        config,
        module_ref,
    )

    assert resolved["owner"]["kind"] == "CommonModule"
    assert resolved["owner"]["name"] == "РасширенныйОбщийМодуль"
    assert resolved["module_payload"]["extension"] == {
        "guid": extension_guid,
        "name": "EF_UPO_SODECSO",
    }
    with adapter_server.cache_connection() as conn:
        code_row = conn.execute(
            "SELECT owner_name, extension_guid, extension_name FROM metadata_code_index_cache WHERE module_ref=?",
            (module_ref,),
        ).fetchone()
        route_row = conn.execute(
            "SELECT manifest_entries_json FROM extension_route_cache WHERE descriptor_cas_key=?",
            (descriptor_key,),
        ).fetchone()
    assert dict(code_row) == {
        "owner_name": "РасширенныйОбщийМодуль",
        "extension_guid": extension_guid,
        "extension_name": "EF_UPO_SODECSO",
    }
    assert module_key in route_row["manifest_entries_json"]


def test_extension_metadata_payload_kind_recognizes_common_module_descriptor(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    atom = lambda value: {"type": "atom", "value": value}
    monkeypatch.setattr(
        adapter_server,
        "parse_config_tree_from_bytes",
        lambda data: {
            "type": "list",
            "items": [
                atom("1"),
                {"type": "list", "items": [atom("1"), {"type": "list", "items": []}]},
                atom("0"),
            ],
        },
    )

    kind = adapter_server.extension_metadata_payload_kind(b"descriptor", {}, {})

    assert kind == "CommonModule"


def test_module_owner_cache_backfill_prefers_existing_code_index(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    config = {"server": "sql.example", "database": "upo_test"}
    owner_guid = "05a138aa-b3c0-48c6-94ea-0acd4fc000cf"
    module_ref = f"Config:{owner_guid}.0#stream:4"
    text = "Функция ЭтоДокумент()\nКонецФункции"
    adapter_server.code_index_upsert(
        config,
        base_id="upo_test",
        table="Config",
        file_name=f"{owner_guid}.0",
        module_ref=module_ref,
        data=text.encode("utf-8"),
        text=text,
    )
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    monkeypatch.setattr(
        adapter_server,
        "list_objects",
        lambda *args, **kwargs: {
            "status": "ok",
            "objects": [
                {
                    "kind": "CommonModule",
                    "name": "ИнтеграцияС1СДокументооборотКлиентСервер",
                    "guid": owner_guid,
                }
            ],
            "counts": {"total": 1, "total_visible": 1},
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_modules",
        lambda payload: pytest.fail("existing code-index rows must not trigger metadata object decoding"),
    )

    result = adapter_server.metadata_module_owner_cache_backfill(
        {"base_id": "upo_test", "kind": "CommonModule", "limit": 1}
    )

    assert result["status"] == "complete"
    assert result["counts"]["objects_resolved_from_code_index"] == 1
    assert result["counts"]["objects_resolved_from_metadata"] == 0
    assert result["objects"][0]["resolved_by"] == "code_index_guid_prefix"
    assert adapter_server.metadata_module_owner_cache_lookup(config, module_ref)["owner"]["name"] == (
        "ИнтеграцияС1СДокументооборотКлиентСервер"
    )


def test_code_index_verify_reports_cache_hit_verified(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    config = {"server": "sql.example", "database": "upo_test"}
    text = "Процедура Проверка()\nКонецПроцедуры"
    data = b"payload"
    module_ref = "ConfigCAS:file1"

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (data, config, None))
    monkeypatch.setattr(adapter_server, "extract_code_index_text_from_payload", lambda *args, **kwargs: (text, {"status": "ok"}))
    adapter_server.code_index_upsert(
        config,
        base_id="upo_test",
        table="ConfigCAS",
        file_name="file1",
        module_ref=module_ref,
        data=data,
        text=text,
        verified=True,
    )

    result = adapter_server.metadata_code_index_verify({"base_id": "upo_test", "module_ref": module_ref})

    assert result["status"] == "ok"
    assert result["freshness"]["status"] == "cache_hit_verified"
    assert result["freshness"]["verified_against_sql"] is True


def test_code_index_verify_reports_cache_hit_stale(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    config = {"server": "sql.example", "database": "upo_test"}
    module_ref = "ConfigCAS:file1"

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    adapter_server.code_index_upsert(
        config,
        base_id="upo_test",
        table="ConfigCAS",
        file_name="file1",
        module_ref=module_ref,
        data=b"old",
        text="Процедура Старая()\nКонецПроцедуры",
        verified=True,
    )
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (b"new", config, None))
    monkeypatch.setattr(adapter_server, "extract_code_index_text_from_payload", lambda *args, **kwargs: ("Процедура Новая()\nКонецПроцедуры", {"status": "ok"}))

    result = adapter_server.metadata_code_index_verify({"base_id": "upo_test", "module_ref": module_ref})

    assert result["status"] == "stale"
    assert result["freshness"]["status"] == "cache_hit_stale"
    assert result["freshness"]["current_payload_sha1"] != result["freshness"]["payload_sha1"]


def test_code_index_search_filters_candidates_by_public_owner_ref(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    config = {"server": "sql.example", "database": "upo_test"}
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    for file_name, owner_name in (("writeoff", "СписаниеЗапасов"), ("planning", "УОП_ПланированиеМеню")):
        adapter_server.code_index_upsert(
            config,
            base_id="upo_test",
            table="Config",
            file_name=file_name,
            module_ref=f"Config:{file_name}",
            data=file_name.encode(),
            text="Процедура Связь() // УОП_ПланированиеМеню\nКонецПроцедуры",
            owner={"owner_kind": "Document", "owner_name": owner_name},
            verified=True,
        )

    result = adapter_server.metadata_code_index_search(
        {"base_id": "upo_test", "ref": "Document.СписаниеЗапасов", "query": "УОП_ПланированиеМеню", "verify": False, "strict": False}
    )

    assert result["status"] == "ok"
    assert result["query"]["ref"] == "Document.СписаниеЗапасов"
    assert [item["object_ref"] for item in result["matches"]] == ["Document.СписаниеЗапасов"]
    assert result["counts"]["candidates"] == 1


def test_code_vector_search_revalidates_candidates(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    config = {"server": "sql.example", "database": "upo_test"}
    text = "Процедура ЗаменаДомена()\nКонецПроцедуры"
    data = b"payload"
    module_ref = "ConfigCAS:file1"

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (data, config, None))
    monkeypatch.setattr(adapter_server, "extract_code_index_text_from_payload", lambda *args, **kwargs: (text, {"status": "ok"}))
    upserted = adapter_server.code_index_upsert(
        config,
        base_id="upo_test",
        table="ConfigCAS",
        file_name="file1",
        module_ref=module_ref,
        data=data,
        text=text,
        owner={"owner_kind": "CommonForm", "owner_name": "t_Форма", "form_name": "t_Форма"},
        verified=True,
    )
    adapter_server.code_vector_upsert_chunks(config, {**upserted, "module_ref": module_ref, "text": text, "routines": upserted["routines"]})

    result = adapter_server.metadata_code_vector_search({"base_id": "upo_test", "query": "домен", "verify": True})

    assert result["status"] == "ok"
    assert result["source"]["authoritative"] is False
    assert result["matches"][0]["freshness"]["status"] == "cache_hit_verified"
    assert result["matches"][0]["owner"]["name"] == "t_Форма"
    assert result["matches"][0]["object_ref"] == "CommonForm.t_Форма"
    assert result["matches"][0]["object_selector"]["name"] == "t_Форма"
    assert result["freshness"]["snapshot_token"]


def test_code_vector_external_embedding_pending_upsert_and_search(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    config = {"server": "sql.example", "database": "upo_test"}
    text = "Процедура РассчитатьНалог()\nКонецПроцедуры"
    data = b"payload"
    module_ref = "ConfigCAS:tax-module"
    embedding_model = "openai-compatible:test-code-model"

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (data, config, None))
    monkeypatch.setattr(
        adapter_server,
        "current_code_index_text_from_sql",
        lambda **kwargs: (text, {"status": "ok"}),
    )
    indexed = adapter_server.code_index_upsert(
        config,
        base_id="upo_test",
        table="ConfigCAS",
        file_name="tax-module",
        module_ref=module_ref,
        data=data,
        text=text,
        owner={"owner_kind": "CommonModule", "owner_name": "Налоги"},
        verified=True,
    )
    adapter_server.code_vector_upsert_chunks(config, {**indexed, "module_ref": module_ref, "text": text})

    pending = adapter_server.metadata_code_vector_pending(
        {
            "base_id": "upo_test",
            "embedding_model": embedding_model,
            "limit": 10,
            "chunk_kinds": ["routine"],
            "max_text_chars": 1000,
        }
    )
    chunk = pending["chunks"][0]
    stored = adapter_server.metadata_code_vector_embedding_upsert(
        {
            "base_id": "upo_test",
            "chunk_id": chunk["chunk_id"],
            "text_sha1": chunk["text_sha1"],
            "embedding_model": embedding_model,
            "embedding": [1.0, 0.0, 0.0],
        }
    )
    result = adapter_server.metadata_code_vector_search(
        {
            "base_id": "upo_test",
            "query": "где считается налог",
            "query_embedding": [1.0, 0.0, 0.0],
            "embedding_model": embedding_model,
            "strict": True,
            "verify": True,
        }
    )

    assert pending["status"] == "ok"
    assert pending["query"]["chunk_kinds"] == ["routine"]
    assert pending["query"]["max_text_chars"] == 1000
    assert chunk["chunk"]["kind"] == "routine"
    assert chunk["text_sha1"] == indexed["text_sha1"]
    assert "РассчитатьНалог" in chunk["text"]
    assert stored["status"] == "ok"
    assert stored["dimensions"] == 3
    assert result["status"] == "ok"
    assert result["source"]["embedding_model"] == embedding_model
    assert result["matches"][0]["object_ref"] == "CommonModule.Налоги"
    assert result["matches"][0]["freshness"]["status"] == "cache_hit_verified"


def test_code_vector_external_embedding_rejects_changed_chunk(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    config = {"server": "sql.example", "database": "upo_test"}
    module_ref = "ConfigCAS:file1"
    old_text = "Процедура Старая()\nКонецПроцедуры"
    new_text = "Процедура Новая()\nКонецПроцедуры"

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    old_index = adapter_server.code_index_upsert(
        config,
        base_id="upo_test",
        table="ConfigCAS",
        file_name="file1",
        module_ref=module_ref,
        data=b"old",
        text=old_text,
        verified=True,
    )
    adapter_server.code_vector_upsert_chunks(config, {**old_index, "module_ref": module_ref, "text": old_text})
    old_chunk = adapter_server.metadata_code_vector_pending(
        {"base_id": "upo_test", "embedding_model": "test:model", "limit": 1}
    )["chunks"][0]

    new_index = adapter_server.code_index_upsert(
        config,
        base_id="upo_test",
        table="ConfigCAS",
        file_name="file1",
        module_ref=module_ref,
        data=b"new",
        text=new_text,
        verified=True,
    )
    adapter_server.code_vector_upsert_chunks(config, {**new_index, "module_ref": module_ref, "text": new_text})
    rejected = adapter_server.metadata_code_vector_embedding_upsert(
        {
            "base_id": "upo_test",
            "chunk_id": old_chunk["chunk_id"],
            "text_sha1": old_chunk["text_sha1"],
            "embedding_model": "test:model",
            "embedding": [1.0, 0.0],
        }
    )

    assert rejected["status"] == "conflict"
    assert rejected["error"] == "chunk_missing_or_changed"


def test_code_vector_search_strict_refreshes_and_drops_stale_text(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    config = {"server": "sql.example", "database": "upo_test"}
    module_ref = "ConfigCAS:file1"

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (b"new-payload", config, None))
    monkeypatch.setattr(
        adapter_server,
        "current_code_index_text_from_sql",
        lambda **kwargs: ("Процедура СовсемДругая()\nКонецПроцедуры", {"status": "ok"}),
    )
    indexed = adapter_server.code_index_upsert(
        config,
        base_id="upo_test",
        table="ConfigCAS",
        file_name="file1",
        module_ref=module_ref,
        data=b"old-payload",
        text="Процедура ЗаменаДомена()\nКонецПроцедуры",
        owner={"owner_kind": "CommonModule", "owner_name": "СтарыйМодуль"},
        verified=True,
    )
    adapter_server.code_vector_upsert_chunks(
        config,
        {**indexed, "module_ref": module_ref, "text": "Процедура ЗаменаДомена()\nКонецПроцедуры"},
    )

    result = adapter_server.metadata_code_vector_search(
        {"base_id": "upo_test", "query": "ЗаменаДомена", "verify": True, "strict": True}
    )

    assert result["status"] == "not_found"
    assert result["matches"] == []
    assert result["counts"]["refreshed_modules"] >= 1
    assert adapter_server.code_index_state_status(config)["status"] == "clean"


def test_code_vector_search_uses_live_fallback_while_new_file_is_dirty(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    config = {"server": "sql.example", "database": "upo_test"}

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    monkeypatch.setattr(
        adapter_server,
        "search_modules",
        lambda payload: {
            "schema": "onec_module_search.v1",
            "status": "ok",
            "matches": [{"snippet": {"text": "НоваяПроцедура"}, "owner": {"kind": "CommonModule", "name": "Новый"}}],
            "counts": {"matches": 1},
        },
    )
    adapter_server.code_index_mark_dirty(
        config,
        reason="saved_state_payload_apply",
        source_table="ConfigCASSave",
        file_names=["new-file"],
    )

    result = adapter_server.metadata_code_vector_search(
        {"base_id": "upo_test", "query": "НоваяПроцедура", "verify": True, "strict": True}
    )

    assert result["status"] == "ok"
    assert result["matches"] == []
    assert result["freshness"]["status"] == "dirty_with_live_fallback"
    assert result["freshness"]["snapshot_token"] is None
    assert result["counts"]["live_fallback_matches"] == 1
    assert result["live_fallback"]["freshness"]["status"] == "live_sql_verified"


def test_code_vector_search_saved_overlay_shadows_active_module(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    config = {"server": "sql.example", "database": "upo_test"}
    text = "Процедура НайтиДомен()\nКонецПроцедуры"

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))

    def read_file(_base_id: str, table: str, _file_name: str, **_kwargs):
        return (table.encode("utf-8"), config, None)

    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", read_file)
    monkeypatch.setattr(
        adapter_server,
        "current_code_index_text_from_sql",
        lambda **kwargs: (text, {"status": "ok"}),
    )
    for table, owner_name in (("ConfigCAS", "Основной"), ("ConfigCASSave", "Сохраненный")):
        module_ref = f"{table}:file1"
        indexed = adapter_server.code_index_upsert(
            config,
            base_id="upo_test",
            table=table,
            file_name="file1",
            module_ref=module_ref,
            data=table.encode("utf-8"),
            text=text,
            owner={"owner_kind": "CommonModule", "owner_name": owner_name},
            verified=True,
        )
        adapter_server.code_vector_upsert_chunks(
            config,
            {**indexed, "module_ref": module_ref, "text": text},
        )

    result = adapter_server.metadata_code_vector_search(
        {"base_id": "upo_test", "query": "НайтиДомен", "verify": True, "strict": True, "limit": 10}
    )

    assert result["status"] == "ok"
    assert result["counts"]["shadowed_candidates"] >= 1
    assert {match["owner"]["name"] for match in result["matches"]} == {"Сохраненный"}
    assert all(match["read_selector"]["module_ref"].startswith("ConfigCASSave:") for match in result["matches"])


def test_saved_overlay_tombstone_hides_active_module(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    config = {"server": "sql.example", "database": "upo_test"}
    text = "Процедура УдаленнаяВСохранении()\nКонецПроцедуры"
    module_ref = "ConfigCAS:file1"

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    indexed = adapter_server.code_index_upsert(
        config,
        base_id="upo_test",
        table="ConfigCAS",
        file_name="file1",
        module_ref=module_ref,
        data=b"active",
        text=text,
        verified=True,
    )
    adapter_server.code_vector_upsert_chunks(config, {**indexed, "module_ref": module_ref, "text": text})
    adapter_server.code_index_overlay_tombstone_set(
        config,
        source_table="ConfigCASSave",
        file_name="file1",
        reason="saved_overlay_contains_no_indexable_bsl",
    )

    result = adapter_server.metadata_code_vector_search(
        {"base_id": "upo_test", "query": "УдаленнаяВСохранении", "verify": True, "strict": True}
    )

    assert result["status"] == "not_found"
    assert result["matches"] == []
    assert result["counts"]["shadowed_candidates"] >= 1


def test_saved_state_invalidation_preserves_index_and_marks_target_dirty(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    config = {"server": "sql.example", "database": "upo_test"}
    module_ref = "ConfigCASSave:file1"
    text = "Процедура Проверка()\nКонецПроцедуры"

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    indexed = adapter_server.code_index_upsert(
        config,
        base_id="upo_test",
        table="ConfigCASSave",
        file_name="file1",
        module_ref=module_ref,
        data=b"payload",
        text=text,
        verified=True,
    )
    adapter_server.code_vector_upsert_chunks(config, {**indexed, "module_ref": module_ref, "text": text})

    result = adapter_server.invalidate_adapter_caches_after_saved_state_change(
        "upo_test",
        reason="test_change",
        source_table="ConfigCASSave",
        file_names=["file1"],
        sync_code_index=False,
    )
    with adapter_server.cache_connection() as conn:
        modules = conn.execute("SELECT COUNT(*) AS count FROM metadata_code_index_cache").fetchone()["count"]

    assert result["status"] == "ok"
    assert result["code_index"]["status"] == "dirty"
    assert result["code_index"]["pending_events"] == 1
    assert result["code_index"]["snapshot_token"] is None
    assert modules == 1


def test_code_index_sync_pending_removes_deleted_saved_overlay(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    config = {"server": "sql.example", "database": "upo_test"}
    module_ref = "ConfigCASSave:file1"
    text = "Процедура БольшеНеСохранена()\nКонецПроцедуры"

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    indexed = adapter_server.code_index_upsert(
        config,
        base_id="upo_test",
        table="ConfigCASSave",
        file_name="file1",
        module_ref=module_ref,
        data=b"saved",
        text=text,
        verified=True,
    )
    adapter_server.code_vector_upsert_chunks(config, {**indexed, "module_ref": module_ref, "text": text})
    adapter_server.code_index_mark_dirty(
        config,
        reason="external_saved_overlay_removed",
        source_table="ConfigCASSave",
        file_names=["file1"],
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_code_index_build",
        lambda payload: {
            "schema": "onec_code_index_build.v1",
            "status": "ok",
            "counts": {"scanned_files": 0, "indexed": 0},
        },
    )

    result = adapter_server.metadata_code_index_sync_pending({"base_id": "upo_test"})
    with adapter_server.cache_connection() as conn:
        modules = conn.execute("SELECT COUNT(*) AS count FROM metadata_code_index_cache").fetchone()["count"]
        vectors = conn.execute("SELECT COUNT(*) AS count FROM metadata_code_vector_cache").fetchone()["count"]

    assert result["status"] == "ok"
    assert result["counts"]["removed_modules"] == 1
    assert result["counts"]["remaining_events"] == 0
    assert result["freshness"]["snapshot_token"]
    assert modules == 0
    assert vectors == 0


def test_code_index_poll_changes_discovers_new_external_saved_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    config = {"server": "sql.example", "database": "upo_test"}

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {
            "schema": "onec_storage_files.v1",
            "status": "ok",
            "files": [{"FileName": "external-new"}],
        },
    )

    def sync_pending(_payload: dict) -> dict:
        state = adapter_server.code_index_mark_indexed(
            config,
            source_table="ConfigCASSave",
            file_names=["external-new"],
        )
        return {
            "schema": "onec_code_index_sync_pending.v1",
            "status": "ok",
            "counts": {"remaining_events": state["pending_events"]},
            "freshness": {"index_state": state, "snapshot_token": state["snapshot_token"]},
        }

    monkeypatch.setattr(adapter_server, "metadata_code_index_sync_pending", sync_pending)

    result = adapter_server.metadata_code_index_poll_changes(
        {"base_id": "upo_test", "table": "ConfigCASSave", "max_items": 10}
    )

    assert result["status"] == "ok"
    assert result["counts"]["new_files"] == 1
    assert result["counts"]["remaining_events"] == 0
    assert result["freshness"]["snapshot_token"]
    assert result["tables"][0]["coverage_complete"] is True


def test_code_index_poll_changes_removes_external_deleted_saved_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    config = {"server": "sql.example", "database": "upo_test"}
    module_ref = "ConfigCASSave:deleted-file"
    text = "Процедура УдаленаСнаружи()\nКонецПроцедуры"

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {"schema": "onec_storage_files.v1", "status": "ok", "files": []},
    )
    indexed = adapter_server.code_index_upsert(
        config,
        base_id="upo_test",
        table="ConfigCASSave",
        file_name="deleted-file",
        module_ref=module_ref,
        data=b"saved",
        text=text,
        verified=True,
    )
    adapter_server.code_vector_upsert_chunks(config, {**indexed, "module_ref": module_ref, "text": text})

    result = adapter_server.metadata_code_index_poll_changes(
        {"base_id": "upo_test", "table": "ConfigCASSave", "max_items": 10}
    )
    with adapter_server.cache_connection() as conn:
        modules = conn.execute("SELECT COUNT(*) AS count FROM metadata_code_index_cache").fetchone()["count"]

    assert result["status"] == "ok"
    assert result["counts"]["removed_files"] == 1
    assert result["counts"]["remaining_events"] == 0
    assert modules == 0


def test_extension_common_form_read_selector_points_to_form_decode() -> None:
    selectors = adapter_server.extension_object_read_selectors(
        "upo_test",
        "CommonForm",
        {"name": "t_Форма", "guid": "11111111-2222-3333-4444-555555555555"},
        {"table": "ConfigCAS", "file_name": "11111111-2222-3333-4444-555555555555"},
    )

    assert selectors["form_decode"] == {
        "method": "metadata.form.decode",
        "base_id": "upo_test",
        "kind": "CommonForm",
        "guid": "11111111-2222-3333-4444-555555555555",
        "name": "t_Форма",
        "table": "ConfigCAS",
        "file_name": "11111111-2222-3333-4444-555555555555",
    }
    assert "forms" not in selectors


def test_form_command_button_write_blocks_without_sql_saved_state(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_forms_search",
        lambda payload: {
            "schema": "onec_saved_state_form_search.v1",
            "status": "ok",
            "base_id": payload["base_id"],
            "forms": [],
            "counts": {"forms": 0},
        },
    )

    result = adapter_server.metadata_form_command_button_write(
        {
            "base_id": "upo_test",
            "extension": "test2",
            "object_type": "CommonForm",
            "object_name": "t_Форма",
            "command_name": "РасчетС",
        }
    )

    assert result["status"] == "blocked"
    assert result["error"] == "saved_state_or_xml_form_target_required"
    assert result["target"]["kind"] == "CommonForm"
    assert result["operation"]["command"]["name"] == "РасчетС"
    assert "SQL storage only" in result["diagnostics"]["source_boundary"]
    assert any(item.get("workflow") == "xml_analysis_learning" for item in result["next_resolution"])
    assert not any(item.get("workflow") == "extension_xml_staging" for item in result["next_resolution"])
    rpc_followups = [item for item in result["next_resolution"] if item.get("method")]
    assert rpc_followups
    assert all("params" in item and "payload" not in item for item in rpc_followups)


def test_form_command_button_write_selects_saved_form_by_owner_without_writing(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    owner_guid = "11111111-1111-1111-1111-111111111111"
    form_guid = "22222222-2222-2222-2222-222222222222"
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_forms_search",
        lambda payload: {
            "status": "ok",
            "forms": [
                {
                    "name": "ФормаОбмена",
                    "owner": {"kind": "DataProcessor", "name": "ПервыйОбмен"},
                    "source": {"table": "ConfigSave", "file_name": "first-form"},
                },
                {
                    "name": "ФормаОбмена",
                    "owner": {"kind": "DataProcessor", "name": "ВторойОбмен"},
                    "source": {"table": "ConfigSave", "file_name": "second-form"},
                },
            ],
            "counts": {"forms": 2},
        },
    )

    result = adapter_server.call_method(
        "metadata.form.command_button.write",
        {
            "base_id": "upo_test",
            "ref": "Обработки.ВторойОбмен",
            "object_guid": owner_guid,
            "form_name": "ФормаОбмена",
            "form_guid": form_guid,
            "command_name": "Выполнить",
            "handler_name": "ОбработатьВыполнение",
            "include_storage": True,
        },
    )

    assert result["status"] == "blocked"
    assert result["counts"]["has_concrete_saved_state"] is True
    assert result["target"]["object"] == "ВторойОбмен"
    assert result["target"]["object_guid"] == owner_guid
    assert result["target"]["form"] == "ФормаОбмена"
    assert result["target"]["form_guid"] == form_guid
    assert result["operation"]["command"]["action"] == "ОбработатьВыполнение"
    assert result["saved_state_target"]["file_name"] == "second-form"
    assert result["diagnostics"]["message"].startswith("A saved-state form target exists")


def test_form_command_button_write_help_exposes_guards_and_child_selectors() -> None:
    methods = {
        row["name"]: row
        for row in adapter_server.call_method("help.methods", {})["methods"]
    }
    method = methods["metadata.form.command_button.write"]

    assert method["selector_capabilities"]["read_only"] is False
    assert "allow_saved_state_write" in method["selector_capabilities"]["write_guards"]
    assert "repository_apply_gate" in method["selector_capabilities"]["write_guards"]
    assert method["selector_capabilities"]["child_selector_arguments"] == [
        "command_name",
        "button_name",
        "handler_name",
    ]
    assert method["input_schema"]["properties"]["form_guid"]["description"].endswith(
        "distinct from the owner GUID."
    )


def test_form_command_button_write_reports_saved_state_target_as_plan(monkeypatch: pytest.MonkeyPatch) -> None:
    command_node = '{11,{1,409b9a53-7f7e-4178-86c1-33176c7c7a7a},"ЗаменаДомена",{1,1,{"ru","Замена домена"}},{1,1,{"ru","Замена домена"}},{0,{0,{"B",1},0}},{0,0,0},{4,0,{0},"",-1,-1,1,0,""},"ЗаменаДомена",3,0,0,{0,0},1,0,1,0,0,2,0,0}'
    button_node = '{34,{1,02023637-7868-4a5f-8576-835a76e0c9ba},0,1,{0,{0,{"B",1},0}},1,"ЗаменаДомена",{1,0},1,{1,409b9a53-7f7e-4178-86c1-33176c7c7a7a},{0},3,0,0,0,2,2,0,0,0,{4,4,{0},4},{4,4,{0},4},{4,4,{0},4},{8,3,0,1,100},{0,0,0},0,{4,0,{0},"",-1,-1,1,0,""},1,{"Pattern"},"",2,0,1,{12,{2,02023637-7868-4a5f-8576-835a76e0c9ba},0,0,0,0,"ЗаменаДоменаРасширеннаяПодсказка",{1,0},{1,0},1,0,0,2,2,{4,4,{0},4},{8,3,0,1,100},{0,0,0},1,{5,0,0,3,0,{0,1,0},{4,4,{0},4},{4,4,{0},4},{3,0,{0},0,1,0,48312c09-257f-4b29-b280-284dd89efc1e}},0,1,2,{1,{1,0},0},0,0,1,0,0,1,0,3,3,0,0},{"U"},1,0,0,1,0,0,0,3,3,3,0,0,1,0,0,0,1,0,0,{4,0,{0},"",-1,-1,1,0,""},0,0,0,1,""}'
    form_text = "{4,{" + ",".join(["0"] * 25 + [button_node]) + '},"",{0,0},{0,0},' + "{0,1," + command_node + "},{0,0},{0,0},0,0}"

    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_forms_search",
        lambda payload: {
            "schema": "onec_saved_state_form_search.v1",
            "status": "ok",
            "base_id": payload["base_id"],
            "forms": [
                {
                    "name": "t_Форма",
                    "source": {"table": "ConfigCASSave", "file_name": "form-file"},
                },
                {
                    "name": "tt_Форма3",
                    "source": {"table": "ConfigCASSave", "file_name": "other-form-file"},
                },
            ],
            "counts": {"forms": 2},
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_form_decode",
        lambda payload: {
            "schema": "onec_form_decode.v1",
            "status": "ok",
            "source": {"table": payload["table"], "file_name": payload["file_name"]},
            "form": {"file_name": payload["file_name"]},
            "profile": {
                "commands": [{"path": "5.2", "name": "ЗаменаДомена", "title": "Замена домена", "guids_sample": ["409b9a53-7f7e-4178-86c1-33176c7c7a7a"]}],
                "items": [{"path": "1.25", "name": "ЗаменаДомена", "title": "", "marker": "34"}],
                "command_links": [{"command": "ЗаменаДомена", "handler": "ЗаменаДомена", "command_guid": "409b9a53-7f7e-4178-86c1-33176c7c7a7a"}],
                "button_command_links": [{"button": "ЗаменаДомена", "command_guid": "409b9a53-7f7e-4178-86c1-33176c7c7a7a"}],
            },
        },
    )
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (form_text.encode("utf-8-sig"), {"database": "upo_test"}, None))
    monkeypatch.setattr(adapter_server, "metadata_write_apply_plan_gate", lambda *args, **kwargs: ({"allowed": True}, None))

    result = adapter_server.metadata_form_command_button_write(
        {
            "base_id": "upo_test",
            "object_type": "CommonForm",
            "object_name": "t_Форма",
            "command_name": "РасчетС",
            "allow_saved_state_write": True,
        }
    )

    assert result["status"] == "planned"
    assert result["saved_state_target"] == {"table": "ConfigCASSave", "form": "t_Форма"}
    assert result["saved_state_search"]["counts"]["forms"] == 2
    assert result["saved_state_search"]["selected_form"]["name"] == "t_Форма"
    assert "forms" not in result["saved_state_search"]
    assert result["counts"]["has_concrete_saved_state"] is True
    assert result["proposal"]["status"] == "accepted_for_review"
    assert [edit["mode"] for edit in result["proposal"]["edits"]] == [
        "structural_append_child_preserve_format",
        "structural_append_child_preserve_format",
    ]
    assert result["structural"]["command_append"]["parent_path"] == "5"
    assert result["structural"]["button_append"]["parent_path"] == "1"
    assert result["handler_result"]["source"] == {"table": "ConfigCASSave", "file_name": "form-file", "module_path": "2"}
    assert result["handler_result"]["proposal"]["edits"][0]["path"] == "2"
    assert result["handler_result"]["status"] == "planned"


def test_form_command_button_write_existing_workflow_returns_semantic_verify_and_refresh(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_forms_search",
        lambda payload: {
            "schema": "onec_saved_state_form_search.v1",
            "status": "ok",
            "base_id": payload["base_id"],
            "forms": [{"name": "t_Форма", "source": {"table": "ConfigCASSave", "file_name": "form-file"}}],
            "counts": {"forms": 1},
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_form_decode",
        lambda payload: {
            "schema": "onec_form_decode.v1",
            "status": "ok",
            "source": {"table": payload["table"], "file_name": payload["file_name"]},
            "form": {"file_name": payload["file_name"]},
            "profile": {
                "commands": [{"path": "5.3", "name": "РасчетС", "title": "РасчетС", "guids_sample": ["74959ac4-7d60-4b92-a21f-cc44c409ed91"]}],
                "items": [{"path": "1.66", "name": "РасчетС", "title": "РасчетС", "marker": "34"}],
                "module": {"routines_sample": [{"kind": "Процедура", "name": "РасчетС"}]},
                "command_links": [{"command": "РасчетС", "handler": "РасчетС", "command_guid": "74959ac4-7d60-4b92-a21f-cc44c409ed91"}],
                "button_command_links": [{"button": "РасчетС", "command": "РасчетС", "command_guid": "74959ac4-7d60-4b92-a21f-cc44c409ed91"}],
                "counts": {"commands_total": 2, "items_total": 10, "command_links": 2, "button_command_links": 2},
            },
        },
    )
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (b'{"ok"}', {"database": "upo_test"}, None))
    monkeypatch.setattr(
        adapter_server,
        "form_embedded_module_handler_write_apply",
        lambda *args, **kwargs: {"schema": "onec_form_embedded_module_write.v1", "status": "applied", "applied": True},
    )
    monkeypatch.setattr(
        adapter_server,
        "code_index_refresh_form_embedded_module",
        lambda **kwargs: {"schema": "onec_code_index_refresh_form_module.v1", "status": "updated", "routine_count": 1},
    )
    monkeypatch.setattr(adapter_server, "repository_apply_gate", lambda *args, **kwargs: None)

    result = adapter_server.metadata_form_command_button_write(
        {
            "base_id": "upo_test",
            "extension": "test2",
            "object_type": "CommonForm",
            "object_name": "t_Форма",
            "command_name": "РасчетС",
            "allow_saved_state_write": True,
            "allow_sql_saved_state_apply": True,
            "mode": "apply",
        }
    )

    assert result["status"] == "verified"
    assert result["idempotency"]["status"] == "already_exists"
    assert result["semantic_verify"]["verified"] is True
    assert result["semantic_verify"]["command"]["path"] == "5.3"
    assert result["semantic_verify"]["button"]["path"] == "1.66"
    assert result["code_index_refresh"]["status"] == "updated"


def test_metadata_form_decode_working_selector_prefers_saved_state(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_saved_state_forms_search(payload: dict[str, Any]) -> dict[str, Any]:
        seen["search_payload"] = payload
        return {
            "schema": "onec_saved_state_form_search.v1",
            "status": "ok",
            "base_id": payload["base_id"],
            "forms": [
                {
                    "name": "t_Форма",
                    "form": {"name": "t_Форма", "guid": "form-guid"},
                    "source": {"kind": "live_metadata"},
                    "file": {"FileName": "extension-guid__form-guid.0"},
                }
            ],
            "counts": {"forms": 1},
        }

    def fake_read_storage_file_bytes(base_id: str, table: str, file_name: str, **kwargs: Any) -> tuple[bytes, dict[str, Any], None]:
        seen["read"] = {"base_id": base_id, "table": table, "file_name": file_name}
        return b"{}", {"database": base_id}, None

    monkeypatch.setattr(adapter_server, "metadata_saved_state_forms_search", fake_saved_state_forms_search)
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)
    monkeypatch.setattr(
        adapter_server,
        "parse_config_tree_from_bytes",
        lambda data: parse_brace_text('{4,{0},"",{0,0},{0,0},{0,0},{0,0},{0,0},0,0}'),
    )

    result = adapter_server.metadata_form_decode(
        {
            "base_id": "upo_test",
            "extension": "test2",
            "object_type": "CommonForm",
            "form": "t_Форма",
            "source_state": "working",
            "include_storage": True,
        }
    )

    assert result["status"] == "ok"
    assert seen["search_payload"]["extension"] == "test2"
    assert seen["search_payload"]["tables"] == ["ConfigCASSave"]
    assert seen["read"] == {"base_id": "upo_test", "table": "ConfigCASSave", "file_name": "extension-guid__form-guid.0"}
    assert result["source"]["table"] == "ConfigCASSave"
    assert result["source"]["file_name"] == "extension-guid__form-guid.0"


def test_form_command_button_verify_resolves_saved_state_by_public_selector(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_saved_state_forms_search(payload: dict[str, Any]) -> dict[str, Any]:
        seen["search_payload"] = payload
        return {
            "schema": "onec_saved_state_form_search.v1",
            "status": "ok",
            "base_id": payload["base_id"],
            "forms": [
                {
                    "name": "t_Форма",
                    "form": {"name": "t_Форма", "guid": "form-guid"},
                    "source": {"table": "ConfigCASSave", "file_name": "extension-guid__form-guid.0"},
                },
                {
                    "name": "tt_Форма3",
                    "form": {"name": "tt_Форма3", "guid": "other-form-guid"},
                    "source": {"table": "ConfigCASSave", "file_name": "extension-guid__other-form-guid.0"},
                },
            ],
            "counts": {"forms": 2},
        }

    def fake_form_decode(payload: dict[str, Any]) -> dict[str, Any]:
        seen["decode_payload"] = payload
        return {
            "schema": "onec_form_decode.v1",
            "status": "ok",
            "source": {"table": payload["table"], "file_name": payload["file_name"]},
            "profile": {
                "commands": [{"path": "5.3", "name": "РасчетС", "title": "РасчетС"}],
                "items": [{"path": "1.66", "name": "РасчетС", "title": "РасчетС", "type_name": "Кнопка"}],
                "module": {"routines_sample": [{"kind": "Процедура", "name": "РасчетС"}]},
                "command_links": [{"command": "РасчетС", "handler": "РасчетС"}],
                "button_command_links": [{"button": "РасчетС", "command": "РасчетС"}],
                "counts": {"commands_total": 2, "items_total": 10},
            },
        }

    monkeypatch.setattr(adapter_server, "metadata_saved_state_forms_search", fake_saved_state_forms_search)
    monkeypatch.setattr(
        adapter_server,
        "metadata_form_decode",
        fake_form_decode,
    )

    result = adapter_server.metadata_form_command_button_verify(
        {
            "base_id": "upo_test",
            "extension": "test2",
            "object_type": "CommonForm",
            "form": "t_Форма",
            "command_name": "РасчетС",
        }
    )

    assert result["status"] == "ok"
    assert result["verified"] is True
    assert result["command"]["path"] == "5.3"
    assert result["command"]["form_path"] == "5.3"
    assert result["button"]["path"] == "1.66"
    assert result["button"]["form_path"] == "1.66"
    assert result["saved_state_search"]["counts"]["forms"] == 2
    assert result["saved_state_search"]["selected_form"]["name"] == "t_Форма"
    assert "forms" not in result["saved_state_search"]
    assert seen["search_payload"]["extension"] == "test2"
    assert seen["search_payload"]["include_storage"] is True
    assert seen["decode_payload"]["table"] == "ConfigCASSave"
    assert seen["decode_payload"]["file_name"] == "extension-guid__form-guid.0"

    public_result = adapter_server.call_method(
        "metadata.form.command_button.verify",
        {
            "base_id": "upo_test",
            "extension": "test2",
            "object_type": "CommonForm",
            "form": "t_Форма",
            "command_name": "РасчетС",
            "include_storage": False,
        },
    )
    assert public_result["status"] == "ok"
    assert public_result["command"]["form_path"] == "5.3"
    assert public_result["button"]["form_path"] == "1.66"
    assert "path" not in public_result["command"]
    assert public_result["saved_state_search"]["selected_form"]["name"] == "t_Форма"
    assert "forms" not in public_result["saved_state_search"]


def test_form_command_button_verify_disambiguates_same_form_name_by_owner(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    seen: dict[str, Any] = {}

    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_forms_search",
        lambda payload: {
            "status": "ok",
            "forms": [
                {
                    "name": "ФормаОбмена",
                    "owner": {"kind": "DataProcessor", "name": "ПервыйОбмен"},
                    "source": {"table": "ConfigSave", "file_name": "first-form"},
                },
                {
                    "name": "ФормаОбмена",
                    "owner": {"kind": "DataProcessor", "name": "ВторойОбмен"},
                    "source": {"table": "ConfigSave", "file_name": "second-form"},
                },
            ],
            "counts": {"forms": 2},
        },
    )

    def decode_form(payload: dict[str, Any]) -> dict[str, Any]:
        seen["file_name"] = payload["file_name"]
        return {
            "status": "ok",
            "profile": {
                "commands": [{"path": "5.3", "name": "Выполнить"}],
                "items": [{"path": "1.2", "name": "Выполнить", "type_name": "Кнопка"}],
                "module": {"routines_sample": [{"name": "Выполнить"}]},
                "command_links": [{"command": "Выполнить", "handler": "Выполнить"}],
                "button_command_links": [{"button": "Выполнить", "command": "Выполнить"}],
            },
        }

    monkeypatch.setattr(adapter_server, "metadata_form_decode", decode_form)

    result = adapter_server.call_method(
        "metadata.form.command_button.verify",
        {
            "base_id": "upo_test",
            "ref": "Обработки.ВторойОбмен",
            "form_name": "ФормаОбмена",
            "command_name": "Выполнить",
        },
    )

    assert result["status"] == "ok"
    assert result["verified"] is True
    assert seen["file_name"] == "second-form"
    assert result["query"]["owner"] == {"kind": "DataProcessor", "name": "ВторойОбмен"}
    assert result["query"]["form"] == "ФормаОбмена"


def test_form_command_button_verify_help_exposes_child_name_selectors() -> None:
    methods = {
        row["name"]: row
        for row in adapter_server.call_method("help.methods", {})["methods"]
    }
    method = methods["metadata.form.command_button.verify"]

    assert method["selector_capabilities"]["read_only"] is True
    assert method["selector_capabilities"]["form_selector_arguments"] == ["form", "form_name"]
    assert method["selector_capabilities"]["child_selector_arguments"] == [
        "command_name",
        "button_name",
        "handler_name",
    ]
    assert method["input_schema"]["properties"]["command_name"]["type"] == "string"


def test_form_write_target_verify_reports_writable_saved_state(monkeypatch: pytest.MonkeyPatch) -> None:
    def fake_resolve(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "schema": "onec_form_write_target_resolution.v1",
            "status": "ok",
            "base_id": payload["base_id"],
            "source": {"table": payload["table"], "file_name": "form-file"},
            "form": {"name": "t_Форма"},
            "target": {"section": "commands", "name": "РасчетС"},
            "effective_target": {"section": "commands", "name": "РасчетС"},
            "write_target": {"section": "commands", "name": "РасчетС", "path": "5.3.3.2.1", "status": "ok"},
            "property": {"status": "ok", "path": "5.3.3.2.1", "old": "РасчетС", "new": "Расчет"},
            "counts": {"matches": 1},
            "search": {
                "schema": "onec_saved_state_form_search.v1",
                "status": "ok",
                "base_id": payload["base_id"],
                "forms": [{"name": "t_Форма", "source": {"table": "ConfigCASSave", "file_name": "form-file"}}],
                "counts": {"forms": 1},
            },
        }

    monkeypatch.setattr(adapter_server, "metadata_form_write_target_resolve", fake_resolve)

    result = adapter_server.call_method(
        "metadata.form.write_target.verify",
        {
            "base_id": "upo_test",
            "extension": "test2",
            "form": "t_Форма",
            "command": "РасчетС",
            "property": "title",
            "value": "Расчет",
        },
    )

    assert result["status"] == "ok"
    assert result["verified"] is True
    assert result["writable_now"] is True
    assert result["needs_prepare"] is False
    assert result["source"] == {
        "kind": "saved_state",
        "layer": "extension_saved_state",
    }
    assert result["write_target"]["form_path"] == "5.3.3.2.1"
    assert "path" not in result["write_target"]
    assert result["saved_state_search"]["selected_form"]["name"] == "t_Форма"


def test_form_write_target_verify_reports_prepare_needed(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_form_write_target_resolve",
        lambda payload: {
            "schema": "onec_form_write_target_resolution.v1",
            "status": "not_found",
            "base_id": payload["base_id"],
            "diagnostics": {"message": "Saved-state form was not found."},
            "search": {"schema": "onec_saved_state_form_search.v1", "status": "ok", "base_id": payload["base_id"], "forms": [], "counts": {"forms": 0}},
        },
    )

    result = adapter_server.call_method(
        "metadata.form.write_target.verify",
        {
            "base_id": "upo_test",
            "extension": "test2",
            "object_type": "CommonForm",
            "form": "t_Форма",
            "command": "РасчетС",
            "property": "title",
        },
    )

    assert result["status"] == "needs_prepare"
    assert result["verified"] is False
    assert result["writable_now"] is False
    assert result["needs_prepare"] is True
    assert result["next_resolution"]["method"] == "metadata.saved_state.prepare"
    assert result["next_resolution"]["params"] == {
        "base_id": "upo_test",
        "layer": "extension_saved_state",
        "ref": "CommonForm.t_Форма",
        "kind": "CommonForm",
        "name": "t_Форма",
        "extension": "test2",
        "form": "t_Форма",
        "mode": "plan",
    }
    assert result["saved_state_search"]["counts"]["forms"] == 0


def test_form_write_target_resolve_prefers_exact_form_among_similar_saved_forms(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_forms_search",
        lambda payload: {
            "schema": "onec_saved_state_form_search.v1",
            "status": "ok",
            "base_id": payload["base_id"],
            "forms": [
                {"name": "t_Форма", "form": {"name": "t_Форма"}, "source": {"table": "ConfigCASSave", "file_name": "form-file"}},
                {"name": "tt_Форма3", "form": {"name": "tt_Форма3"}, "source": {"table": "ConfigCASSave", "file_name": "other-form-file"}},
            ],
            "counts": {"forms": 2},
        },
    )

    def fake_form_decode(payload: dict[str, Any]) -> dict[str, Any]:
        seen["decode_payload"] = payload
        return {
            "schema": "onec_form_decode.v1",
            "status": "ok",
            "source": {"table": payload["table"], "file_name": payload["file_name"]},
            "form": {"name": "t_Форма"},
            "profile": {
                "commands": [{"path": "5.3", "name": "РасчетС", "title": "РасчетС", "_profile_section": "commands"}],
                "items": [],
                "attributes": [],
                "tables": [],
                "command_bars": [],
            },
        }

    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_form_decode)

    result = adapter_server.metadata_form_write_target_resolve(
        {
            "base_id": "upo_test",
            "extension": "test2",
            "form": "t_Форма",
            "command": "РасчетС",
            "property": "title",
        }
    )

    assert result["status"] == "ok"
    assert seen["decode_payload"]["file_name"] == "form-file"
    assert result["target"]["name"] == "РасчетС"


def test_form_write_target_resolve_disambiguates_same_form_name_by_owner(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}
    first_owner_guid = "11111111-1111-1111-1111-111111111111"
    second_owner_guid = "22222222-2222-2222-2222-222222222222"

    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_forms_search",
        lambda payload: {
            "schema": "onec_saved_state_form_search.v1",
            "status": "ok",
            "base_id": payload["base_id"],
            "forms": [
                {
                    "name": "ФормаОбмена",
                    "form": {"name": "ФормаОбмена"},
                    "owner": {"kind": "DataProcessor", "name": "ПервыйОбмен", "guid": first_owner_guid},
                    "source": {"table": "ConfigCASSave", "file_name": "first-form"},
                },
                {
                    "name": "ФормаОбмена",
                    "form": {"name": "ФормаОбмена"},
                    "owner": {"kind": "DataProcessor", "name": "ВторойОбмен", "guid": second_owner_guid},
                    "source": {"table": "ConfigCASSave", "file_name": "second-form"},
                },
            ],
            "counts": {"forms": 2},
        },
    )

    def fake_form_decode(payload: dict[str, Any]) -> dict[str, Any]:
        seen["decode_payload"] = payload
        return {
            "schema": "onec_form_decode.v1",
            "status": "ok",
            "source": {"table": payload["table"], "file_name": payload["file_name"]},
            "form": {"name": "ФормаОбмена"},
            "profile": {
                "commands": [{"path": "5.3", "name": "РасчетС", "title": "РасчетС", "_profile_section": "commands"}],
                "items": [],
                "attributes": [],
                "tables": [],
                "command_bars": [],
            },
        }

    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_form_decode)

    result = adapter_server.call_method(
        "metadata.form.write_target.resolve",
        {
            "base_id": "upo_test",
            "ref": "Обработки.ВторойОбмен",
            "object_guid": second_owner_guid,
            "form_name": "ФормаОбмена",
            "command": "РасчетС",
            "property": "title",
        },
    )

    assert result["status"] == "ok"
    assert seen["decode_payload"]["file_name"] == "second-form"
    assert "guid" not in seen["decode_payload"]
    assert result["target"]["name"] == "РасчетС"


def test_form_write_target_help_separates_owner_form_and_child_selectors() -> None:
    methods = {
        row["name"]: row
        for row in adapter_server.call_method("help.methods", {})["methods"]
    }

    for method_name in ("metadata.form.write_target.resolve", "metadata.form.write_target.verify"):
        method = methods[method_name]
        assert method["selector_capabilities"]["accepts_ordinal"] is False
        assert method["selector_capabilities"]["read_only"] is True
        assert method["selector_capabilities"]["form_selector_arguments"] == ["form", "form_name", "form_guid"]
        assert method["selector_capabilities"]["child_selector_arguments"] == [
            "element",
            "element_name",
            "command",
            "attribute",
        ]
        properties = method["input_schema"]["properties"]
        assert properties["object_guid"]["description"].endswith("owner GUID.")
        assert properties["form_guid"]["description"].endswith("owner GUID.")


def test_saved_state_forms_search_names_saved_only_extension_form(monkeypatch: pytest.MonkeyPatch) -> None:
    form_file = "ext-guid__caee4437-14b6-436f-98e3-360c4577f20d.0"
    descriptor_file = "ext-guid__caee4437-14b6-436f-98e3-360c4577f20d"
    seen_reads: list[tuple[str, str, str]] = []

    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {
            "status": "ok",
            "files": [{"FileName": form_file, "PartCount": 1, "Bytes": 100}],
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_form_decode",
        lambda payload: {
            "schema": "onec_form_decode.v1",
            "status": "ok",
            "source": {"kind": "live_sql", "table": payload["table"], "file_name": payload["file_name"]},
            "form": {"guid": "caee4437-14b6-436f-98e3-360c4577f20d", "file_name": payload["file_name"]},
            "profile": {"items": []},
            "counts": {"items": 0},
        },
    )

    def fake_read_storage_file_bytes(base_id: str, table: str, file_name: str, **kwargs: Any) -> tuple[bytes | None, dict[str, Any], None]:
        seen_reads.append((base_id, table, file_name))
        assert file_name == descriptor_file
        return b"descriptor", {"database": base_id}, None

    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)
    monkeypatch.setattr(
        adapter_server,
        "config_identity_from_bytes",
        lambda data: {
            "guid": "caee4437-14b6-436f-98e3-360c4577f20d",
            "name": "new_SaveOnlyForm",
            "synonyms": {"ru": "Новая форма"},
        },
    )

    result = adapter_server.metadata_saved_state_forms_search(
        {
            "base_id": "upo_test",
            "tables": ["ConfigCASSave"],
            "form": "new_SaveOnlyForm",
            "limit": 10,
            "include_storage": True,
        }
    )

    assert result["status"] == "ok"
    assert result["counts"]["forms"] == 1
    row = result["forms"][0]
    assert row["name"] == "new_SaveOnlyForm"
    assert row["synonym"] == "Новая форма"
    assert row["form"]["name"] == "new_SaveOnlyForm"
    assert row["form"]["synonym"] == "Новая форма"
    assert row["form"]["identity"]["source"] == "saved_state_descriptor"
    assert row["form"]["identity"]["descriptor_file_name"] == descriptor_file
    assert seen_reads == [("upo_test", "ConfigCASSave", descriptor_file)]


def test_saved_state_forms_search_disambiguates_same_form_name_by_owner(monkeypatch: pytest.MonkeyPatch) -> None:
    owner_guids = {
        "first-form.0": "11111111-1111-1111-1111-111111111111",
        "second-form.0": "22222222-2222-2222-2222-222222222222",
    }
    owner_names = {
        "first-form.0": "ПервыйОбмен",
        "second-form.0": "ВторойОбмен",
    }

    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {
            "status": "ok",
            "files": [{"FileName": file_name} for file_name in owner_guids],
        },
    )

    def fake_row(**kwargs: Any) -> dict[str, Any]:
        file_name = kwargs["file_name"]
        return {
            "table": kwargs["table"],
            "file_name": file_name,
            "name": "ФормаОбмена",
            "form": {"name": "ФормаОбмена", "guid": f"{file_name}-guid"},
            "owner": {
                "kind": "DataProcessor",
                "name": owner_names[file_name],
                "guid": owner_guids[file_name],
            },
            "source": {"table": kwargs["table"], "file_name": file_name},
            "matches": [],
        }

    monkeypatch.setattr(adapter_server, "saved_state_form_search_row", fake_row)

    result = adapter_server.call_method(
        "metadata.saved_state.forms.search",
        {
            "base_id": "upo_test",
            "tables": ["ConfigCASSave"],
            "ref": "Обработки.ВторойОбмен",
            "object_guid": owner_guids["second-form.0"],
            "form_name": "ФормаОбмена",
            "include_storage": True,
        },
    )

    assert result["status"] == "ok"
    assert result["counts"]["forms"] == 1
    assert result["forms"][0]["file_name"] == "second-form.0"
    assert result["query"]["owner"] == {
        "kind": "DataProcessor",
        "name": "ВторойОбмен",
        "guid": owner_guids["second-form.0"],
    }
    assert result["query"]["form"] == "ФормаОбмена"


def test_saved_state_forms_search_default_is_name_first(monkeypatch: pytest.MonkeyPatch) -> None:
    file_name = "extension-guid__form-guid.0"
    monkeypatch.setattr(adapter_server, "extension_filter_to_guid", lambda *args, **kwargs: ("extension-guid", None))
    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {"status": "ok", "files": [{"FileName": file_name, "PartCount": 1, "Bytes": 100}]},
    )
    monkeypatch.setattr(
        adapter_server,
        "saved_state_form_search_row",
        lambda **kwargs: {
            "table": "ConfigCASSave",
            "file_name": file_name,
            "name": "t_Форма",
            "synonym": "T форма",
            "owner": {
                "status": "resolved",
                "kind": "Catalog",
                "name": "test2",
                "guid": "owner-guid",
                "source": "saved_state_descriptor",
            },
            "form": {
                "name": "t_Форма",
                "synonym": "T форма",
                "guid": "form-guid",
                "file_name": file_name,
                "identity": {"guid": "form-guid", "descriptor_file_name": "extension-guid__form-guid"},
            },
            "source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": file_name},
            "counts": {"items": 1, "commands": 1},
            "matches": [
                {
                    "name": "ЗаменаДомена",
                    "id": "1",
                    "title": "Замена домена",
                    "path": "5.2",
                    "marker": "11",
                    "section": "commands",
                    "writable_properties": [
                        {
                            "property": "title",
                            "presentation": "Заголовок",
                            "path": "5.2.3.2.1",
                            "value": "Замена домена",
                            "value_type": "string",
                            "verification": "source_aware_readback",
                        }
                    ],
                }
            ],
        },
    )

    result = adapter_server.metadata_saved_state_forms_search(
        {
            "base_id": "upo_test",
            "tables": ["ConfigCASSave"],
            "extension": "test2",
            "ref": "Catalog.test2",
            "form": "t_Форма",
            "command": "ЗаменаДомена",
        }
    )

    assert result["source"] == {"kind": "saved_state"}
    assert result["query"]["owner"]["ref"] == "Catalog.test2"
    assert result["counts"] == {"forms": 1, "limit": 50}
    form = result["forms"][0]
    assert form["qualified_name"] == "test2.t_Форма"
    assert form["selector"] == {
        "extension": "test2",
        "ref": "Catalog.test2",
        "form": "t_Форма",
        "qualified_name": "test2.t_Форма",
    }
    assert form["matches"][0]["selector"] == {
        "extension": "test2",
        "ref": "Catalog.test2",
        "form": "t_Форма",
        "qualified_name": "test2.t_Форма",
        "command": "ЗаменаДомена",
    }
    assert form["matches"][0]["writable_properties"] == [
        {
            "property": "title",
            "presentation": "Заголовок",
            "value": "Замена домена",
            "value_type": "string",
            "verification": "source_aware_readback",
        }
    ]
    for storage_marker in (
        "ConfigCASSave",
        "extension-guid",
        "form-guid",
        "owner-guid",
        "file_name",
        "descriptor_file_name",
        "path",
        "marker",
        "live_sql",
    ):
        assert storage_marker not in str(result)


def test_saved_state_forms_search_infers_public_extension_name_without_filter(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    file_name = f"{extension_guid}__form-guid.0"
    monkeypatch.setattr(
        adapter_server,
        "extension_map_by_guid",
        lambda base_id: {extension_guid: {"guid": extension_guid, "name": "test2"}},
    )
    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {"status": "ok", "files": [{"FileName": file_name}]},
    )
    monkeypatch.setattr(
        adapter_server,
        "saved_state_form_search_row",
        lambda **kwargs: {
            "table": "ConfigCASSave",
            "file_name": file_name,
            "name": "t_Форма",
            "owner": {"status": "resolved", "kind": "Catalog", "name": "test2"},
            "form": {"name": "t_Форма"},
            "source": {"table": "ConfigCASSave", "file_name": file_name},
            "matches": [
                {
                    "name": "ЗаменаДомена",
                    "section": "commands",
                    "writable_properties": [
                        {
                            "property": "title",
                            "presentation": "Заголовок",
                            "value": "Замена домена",
                            "value_type": "string",
                        }
                    ],
                }
            ],
        },
    )

    result = adapter_server.metadata_saved_state_forms_search(
        {
            "base_id": "upo_test",
            "tables": ["ConfigCASSave"],
            "limit": 1,
        }
    )

    form = result["forms"][0]
    assert form["selector"]["extension"] == "test2"
    assert form["matches"][0]["selector"] == {
        "extension": "test2",
        "ref": "Catalog.test2",
        "form": "t_Форма",
        "qualified_name": "test2.t_Форма",
        "command": "ЗаменаДомена",
    }
    assert extension_guid not in str(result)
    assert "ConfigCASSave" not in str(result["forms"])


def test_saved_state_form_search_resolves_owner_for_internal_name_first_write(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_form_decode",
        lambda payload: {
            "status": "ok",
            "source": {"kind": "live_sql", "table": payload["table"], "file_name": payload["file_name"]},
            "form": {"file_name": payload["file_name"]},
            "profile": {"items": [], "commands": []},
            "counts": {"items": 0, "commands": 0},
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "saved_state_form_descriptor_identity",
        lambda **kwargs: {"name": "t_Форма", "synonym": "T форма", "guid": "form-guid"},
    )
    monkeypatch.setattr(
        adapter_server,
        "saved_state_related_descriptor_identity",
        lambda **kwargs: {
            "kind": "Catalog",
            "name": "test2",
            "synonym": "Test2",
            "guid": "owner-guid",
        },
    )

    row = adapter_server.saved_state_form_search_row(
        base_id="upo_test",
        table="ConfigCASSave",
        file_name="extension-guid__form-guid.0",
        payload={"include_storage": True, "form": "t_Форма"},
        timeout_seconds=30,
    )

    assert row is not None
    assert row["owner"]["kind"] == "Catalog"
    assert row["owner"]["name"] == "test2"


def test_saved_state_forms_search_help_exposes_owner_and_form_selectors() -> None:
    method = {
        row["name"]: row
        for row in adapter_server.call_method("help.methods", {})["methods"]
    }["metadata.saved_state.forms.search"]

    assert method["selector_capabilities"]["accepts_ordinal"] is False
    assert method["selector_capabilities"]["read_only"] is True
    assert method["selector_capabilities"]["form_selector_arguments"] == ["form", "form_name", "form_guid"]
    assert method["selector_capabilities"]["child_selector_arguments"] == [
        "element",
        "element_name",
        "command",
        "attribute",
    ]
    assert method["input_schema"]["properties"]["object_guid"]["type"] == "string"
    assert method["input_schema"]["properties"]["form_guid"]["type"] == "string"


def test_access_graph_builds_effective_permissions_with_sources() -> None:
    access = {
        "users": [{"id": "u1", "name": "Иванов", "groups": ["g1"]}],
        "groups": [{"id": "g1", "name": "Менеджеры продаж", "profiles": ["p1"]}],
        "profiles": [{"id": "p1", "name": "Продажи", "roles": ["r1"]}],
        "roles": [
            {
                "id": "r1",
                "name": "ДобавлениеИзменениеЗаказовКлиентов",
                "permissions": [{"object": "Document.ЗаказКлиента", "actions": ["read", "create"]}],
            }
        ],
        "data_restrictions": [{"subject_type": "group", "subject_id": "g1", "dimension": "Организация", "values": ["Ромашка ООО"]}],
    }

    result = call_method("access.graph.build", {"base_id": "upo_test", "access": access})

    assert result["status"] == "ok"
    assert {key: result["counts"][key] for key in ("users", "groups", "profiles", "roles", "data_restrictions", "effective_users")} == {
        "users": 1,
        "groups": 1,
        "profiles": 1,
        "roles": 1,
        "data_restrictions": 1,
        "effective_users": 1,
    }
    assert result["counts"]["effective_permissions_total"] == 2
    effective = result["effective_users"][0]
    assert effective["user"]["name"] == "Иванов"
    assert effective["groups"] == ["g1"]
    assert {item["action"] for item in effective["permissions"]} == {"read", "create"}
    source = effective["permissions"][0]["sources"][0]
    assert source["role"] == "r1"
    assert source["chains"][0] == {"type": "group_profile_role", "group": "g1", "profile": "p1"}
    assert effective["data_restrictions"][0]["dimension"] == "Организация"


def test_access_user_explain_filters_permissions() -> None:
    access = {
        "users": [{"id": "u1", "name": "Иванов", "groups": ["g1"]}],
        "groups": [{"id": "g1", "profiles": ["p1"]}],
        "profiles": [{"id": "p1", "roles": ["r1"]}],
        "roles": [{"id": "r1", "permissions": [{"object": "Catalog.Контрагенты", "actions": ["read", "update"]}]}],
    }

    result = call_method(
        "access.user.explain",
        {"access": access, "user": "Иванов", "object": "Catalog.Контрагенты", "action": "read"},
    )

    assert result["status"] == "ok"
    assert result["user"]["id"] == "u1"
    assert len(result["permissions"]) == 1
    assert result["permissions"][0]["object"] == "Catalog.Контрагенты"
    assert result["permissions"][0]["action"] == "read"
    assert result["permissions"][0]["sources"][0]["chains"][0]["profile"] == "p1"


def test_access_user_explain_normalizes_action_filter() -> None:
    access = {
        "users": [{"id": "u1", "name": "Иванов", "roles": ["r1"]}],
        "roles": [{"id": "r1", "permissions": [{"object": "Catalog.Контрагенты", "actions": ["Просмотр", "Изменение"]}]}],
    }

    result = call_method(
        "access.user.explain",
        {"access": access, "user": "Иванов", "object": "Catalog.Контрагенты", "action": "read"},
    )

    assert result["status"] == "ok"
    assert [permission["action"] for permission in result["permissions"]] == ["Просмотр"]


def test_access_user_explain_resolves_object_filter_from_name(monkeypatch: pytest.MonkeyPatch) -> None:
    object_guid = "38cea62d-22bb-11ed-9fae-0050569db20c"
    monkeypatch.setattr(
        adapter_server,
        "resolve_object_guid",
        lambda payload, base_id, **kwargs: (
            object_guid,
            "InformationRegister",
            {
                "guid": object_guid,
                "kind": "InformationRegister",
                "kind_ru": "РегистрСведений",
                "name": "УОП_АктуальныеСпецификации",
                "synonym": "Актуальные рецептуры",
            },
            None,
        ),
    )
    access = {
        "users": [{"id": "u1", "name": "Иванов", "roles": ["r1"]}],
        "roles": [
            {
                "id": "r1",
                "permissions": [
                    {
                        "object": "000000C4:AAA",
                        "object_name": "Актуальные рецептуры (Регистр сведений)",
                        "actions": ["Добавление", "Изменение"],
                    }
                ],
            }
        ],
    }

    result = call_method(
        "access.user.explain",
        {
            "base_id": "upo_test",
            "access": access,
            "user": "Иванов",
            "object": "РегистрСведений.УОП_АктуальныеСпецификации",
            "action": "write",
        },
    )

    assert result["status"] == "ok"
    assert [permission["object_name"] for permission in result["permissions"]] == ["Актуальные рецептуры (Регистр сведений)"]


def test_access_user_explain_reports_available_object_actions_when_requested_action_missing() -> None:
    access = {
        "users": [{"id": "u1", "name": "Иванов", "roles": ["r1"]}],
        "roles": [
            {
                "id": "r1",
                "permissions": [
                    {
                        "object": "000000C4:AAA",
                        "object_name": "Актуальные рецептуры (Регистр сведений)",
                        "actions": ["Чтение", "ЧтениеБезОграничения"],
                    }
                ],
            }
        ],
    }

    result = call_method(
        "access.user.explain",
        {
            "access": access,
            "user": "Иванов",
            "object": "Актуальные рецептуры",
            "action": "write",
        },
    )

    assert result["status"] == "ok"
    assert result["permissions"] == []
    assert result["available_actions_for_object"] == {"read": True, "insert": False, "update": False, "delete": False}
    assert "other rights" in result["summary"]["hint"]


def test_access_user_explain_not_found_returns_nearest_users() -> None:
    access = {
        "users": [{"id": "u1", "name": "Lyubov.IVANOVA", "roles": []}, {"id": "u2", "name": "Ivan.PETROV", "roles": []}],
    }

    result = call_method("access.user.explain", {"access": access, "user": "Lyubov.EREMINA"})

    assert result["status"] == "not_found"
    assert result["nearest_users"][0]["name"] == "Lyubov.IVANOVA"
    assert result["diagnostics"]["hint"].startswith("Use access.users.search")


def test_access_users_search_returns_matches_and_nearest(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "access_bsp_extractor_plan",
        lambda base_id: (
            {"users": "users_sql"},
            {"users": {"id": "id", "name": "name", "active": "active", "marked": "marked", "user_type": "user_type"}},
            {"preset": "bsp"},
        ),
    )
    monkeypatch.setattr(
        adapter_server,
        "access_rows_from_query",
        lambda base_id, query, *, limit, timeout_seconds: (
            [
                {"id": "u1", "name": "Lyubov.IVANOVA", "active": True, "marked": False, "user_type": "user"},
                {"id": "u2", "name": "Ivan.PETROV", "active": True, "marked": False, "user_type": "user"},
            ],
            None,
            False,
        ),
    )

    result = adapter_server.access_users_search({"base_id": "upo_test", "query": "Lyubov", "limit": 5})

    assert result["status"] == "ok"
    assert [user["name"] for user in result["users"]] == ["Lyubov.IVANOVA"]
    assert result["counts"]["scanned"] == 2
    assert result["user_kind"] == "bsp_catalog_user"
    assert result["terminology"]["authoritative_for_platform_roles"] is False
    assert result["terminology"]["default_user_method"] == "infobase.users.search"


def test_infobase_users_search_is_default_configurator_user_source(monkeypatch: pytest.MonkeyPatch) -> None:
    users = [
        adapter_server.infobase_user_public(
            {
                "id": "8fca349abe4d635d430f0c6c4e749dcd",
                "name": "Администратор 1С",
                "full_name": "Администратор 1С",
                "changed": "2026-01-01T00:00:00",
                "role_set_id": "4",
                "visible_in_login_list": 1,
                "standard_authentication_enabled": 1,
                "os_authentication_configured": 0,
                "email_configured": 0,
                "platform_administrator": 1,
                "protected_data_bytes": 495,
            }
        ),
        adapter_server.infobase_user_public(
            {
                "id": "a74272fec8cf262a405a568d7871fb07",
                "name": "Аналитик",
                "full_name": "Аналитик",
                "role_set_id": "363",
                "visible_in_login_list": 0,
                "standard_authentication_enabled": 0,
                "os_authentication_configured": 0,
                "email_configured": 0,
                "platform_administrator": 0,
                "protected_data_bytes": 703,
            }
        ),
    ]
    monkeypatch.setattr(adapter_server, "infobase_users_read", lambda *args, **kwargs: (users, None))

    result = call_method("infobase.users.search", {"base_id": "upo_test", "query": "Аналитик"})

    assert result["status"] == "ok"
    assert result["terminology"]["default_user_meaning"] == "infobase_user"
    assert result["terminology"]["bsp_user_is_separate"] is True
    assert result["source"]["table"] == "dbo.v8users"
    assert result["users"][0]["name"] == "Аналитик"
    assert result["users"][0]["role_set_id"] == "363"
    assert result["users"][0]["role_assignment"]["exact_role_names_status"] == "runtime_required"
    assert result["capabilities"]["password_hashes_exposed"] is False
    assert "protected_data" not in result["users"][0]


def test_infobase_user_get_does_not_substitute_bsp_roles(monkeypatch: pytest.MonkeyPatch) -> None:
    user = adapter_server.infobase_user_public(
        {
            "id": "8fca349abe4d635d430f0c6c4e749dcd",
            "name": "Администратор 1С",
            "full_name": "Администратор 1С",
            "role_set_id": "4",
            "platform_administrator": 1,
            "protected_data_bytes": 495,
        }
    )
    monkeypatch.setattr(adapter_server, "infobase_users_read", lambda *args, **kwargs: ([user], None))

    result = call_method("infobase.user.get", {"base_id": "upo_test", "user": "Администратор 1С"})

    assert result["status"] == "ok"
    assert result["user"]["user_kind"] == "infobase_user"
    assert result["user"]["platform_administrator"] is True
    assert result["user"]["role_assignment"]["exact_role_names"] is None
    assert result["bsp_correlation"]["authoritative_for_platform_roles"] is False


def test_infobase_user_password_capabilities_block_anonymous_mutations(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.delenv("ONEC_ADAPTER_SERVICE_TOKEN", raising=False)
    monkeypatch.delenv("ONEC_INFOBASE_USER_ADMIN_ALLOW_UNAUTHENTICATED", raising=False)
    monkeypatch.setattr(
        adapter_server,
        "infobase_user_admin_config_for_base",
        lambda base_id: ({"url": "https://runtime.example", "token": "bridge-token"}, None),
    )

    result = call_method("infobase.user.password.capabilities", {"base_id": "upo_test"})

    assert result["status"] == "blocked"
    assert result["operations"] == {"status": False, "set": False, "clear": False}
    assert result["blocker"]["status"] == "service_auth_required"
    assert result["security"]["direct_sql_password_write"] is False


@pytest.mark.parametrize(
    "standard_authentication_enabled,empty_hashes,expected_state,can_login_empty",
    [(1, True, "empty", True), (1, False, "set", False), (0, True, "standard_authentication_disabled", False)],
)
def test_infobase_user_password_status_is_safe(
    monkeypatch: pytest.MonkeyPatch,
    standard_authentication_enabled: int,
    empty_hashes: bool,
    expected_state: str,
    can_login_empty: bool,
) -> None:
    platform_id = "b58c9366f69fa7f54b687267775eb46c"
    hash_value = adapter_server.INFOBASE_EMPTY_PASSWORD_SHA1_BASE64 if empty_hashes else base64.b64encode(hashlib.sha1(b"set").digest()).decode("ascii")
    monkeypatch.setattr(
        adapter_server,
        "infobase_user_get",
        lambda payload: {
            "status": "ok",
            "user": {"id": platform_id.upper(), "name": "Администратор 1С УПО 3", "platform_administrator": True},
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "infobase_user_password_data_decode",
        lambda data: {"password_pair": (12, 13), "values": ([None] * 12) + [hash_value, hash_value]},
    )

    class FakeCursor:
        def __enter__(self):
            return self

        def __exit__(self, exc_type, exc, tb):
            return False

        def execute(self, query: str, params: tuple[object, ...]) -> None:
            pass

        def fetchone(self) -> dict[str, object]:
            return {
                "Name": "Администратор 1С УПО 3",
                "standard_authentication_enabled": standard_authentication_enabled,
                "Data": b"protected",
            }

    class FakeConnection:
        def __enter__(self):
            return self

        def __exit__(self, exc_type, exc, tb):
            return False

        def cursor(self, *, as_dict: bool = False) -> FakeCursor:
            assert as_dict is True
            return FakeCursor()

        def close(self) -> None:
            pass

    monkeypatch.setattr(
        adapter_server,
        "connect_live_sql",
        lambda base_id, method, *, timeout_seconds: (FakeConnection(), {"database": "upo"}, None),
    )

    result = call_method("infobase.user.password.status", {"base_id": "upo", "user": "Администратор 1С УПО 3"})

    assert result["status"] == "ok"
    assert result["password_state"] == expected_state
    assert result["can_login_with_empty_password"] is can_login_empty
    assert result["security"] == {"password_hashes_exposed": False, "protected_data_exposed": False}
    assert hash_value not in json.dumps(result, ensure_ascii=False)


def test_infobase_user_password_set_requires_service_auth(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.delenv("ONEC_ADAPTER_SERVICE_TOKEN", raising=False)
    monkeypatch.delenv("ONEC_INFOBASE_USER_ADMIN_ALLOW_UNAUTHENTICATED", raising=False)

    result = call_method(
        "infobase.user.password.set",
        {
            "base_id": "upo_test",
            "user": "Аналитик",
            "confirm_user_id": "a74272fec8cf262a405a568d7871fb07",
            "new_password": "temporary-test-password",
            "allow_password_change": True,
        },
    )

    assert result["status"] == "blocked"
    assert result["error"] == "service_auth_required"
    assert "temporary-test-password" not in json.dumps(result, ensure_ascii=False)


def test_infobase_user_password_set_uses_sql_without_echoing_secret(monkeypatch: pytest.MonkeyPatch) -> None:
    platform_id = "a74272fec8cf262a405a568d7871fb07"
    captured: dict[str, object] = {}
    monkeypatch.delenv("ONEC_ADAPTER_SERVICE_TOKEN", raising=False)
    monkeypatch.setenv("ONEC_INFOBASE_USER_ADMIN_ALLOW_UNAUTHENTICATED", "true")
    monkeypatch.setattr(
        adapter_server,
        "infobase_user_get",
        lambda payload: {
            "status": "ok",
            "user": {"id": platform_id.upper(), "name": "Аналитик", "platform_administrator": False},
        },
    )
    def fake_sql_set(base_id: str, user: dict[str, object], *, new_password: str, request_id: str, timeout_seconds: int):
        captured.update(
            {"base_id": base_id, "user": user, "new_password": new_password, "request_id": request_id, "timeout_seconds": timeout_seconds}
        )
        return {
            "status": "ok",
            "applied": True,
            "verification": {"only_current_password_hash_pair_changed": True},
        }

    monkeypatch.setattr(adapter_server, "infobase_user_password_set_sql", fake_sql_set)
    result = call_method(
        "infobase.user.password.set",
        {
            "base_id": "upo_test",
            "user": "Аналитик",
            "confirm_user_id": platform_id,
            "new_password": "temporary-test-password",
            "allow_password_change": True,
            "request_id": "request-1",
        },
    )

    assert result["status"] == "ok"
    assert result["applied"] is True
    assert captured["base_id"] == "upo_test"
    assert captured["new_password"] == "temporary-test-password"
    assert "temporary-test-password" not in json.dumps(result, ensure_ascii=False)
    assert result["verification"]["only_current_password_hash_pair_changed"] is True


def test_infobase_user_password_clear_has_no_password_and_admin_extra_gate(monkeypatch: pytest.MonkeyPatch) -> None:
    platform_id = "8fca349abe4d635d430f0c6c4e749dcd"
    captured: dict[str, object] = {}
    monkeypatch.setenv("ONEC_ADAPTER_SERVICE_TOKEN", "adapter-service-token")
    monkeypatch.setattr(
        adapter_server,
        "infobase_user_get",
        lambda payload: {
            "status": "ok",
            "user": {"id": platform_id, "name": "Администратор 1С", "platform_administrator": True},
        },
    )

    blocked = call_method(
        "infobase.user.password.clear",
        {
            "base_id": "upo_test",
            "user": "Администратор 1С",
            "confirm_user_id": platform_id,
            "allow_password_clear": True,
        },
    )
    assert blocked["status"] == "invalid_argument"
    assert blocked["argument"] == "allow_administrator_password_change"

    def fake_sql_clear(base_id: str, user: dict[str, object], *, request_id: str, timeout_seconds: int):
        captured.update({"base_id": base_id, "user": user, "request_id": request_id, "timeout_seconds": timeout_seconds})
        return {"status": "ok", "operation": "clear", "applied": True}

    monkeypatch.setattr(adapter_server, "infobase_user_password_clear_sql", fake_sql_clear)
    result = call_method(
        "infobase.user.password.clear",
        {
            "base_id": "upo_test",
            "user": "Администратор 1С",
            "confirm_user_id": platform_id,
            "allow_password_clear": True,
            "allow_administrator_password_change": True,
        },
    )

    assert result["status"] == "ok"
    assert captured["base_id"] == "upo_test"
    assert captured["user"]["id"] == platform_id
    assert "new_password" not in captured


@pytest.mark.parametrize("field_count,pair_start", [(31, 11), (34, 11), (33, 12), (38, 12), (40, 12), (43, 12)])
def test_infobase_user_password_data_clear_preserves_container(field_count: int, pair_start: int) -> None:
    old_hash = base64.b64encode(hashlib.sha1(b"old-test-password").digest()).decode("ascii")
    fields = ["0"] * field_count
    fields[7] = "1"
    fields[pair_start] = json.dumps(old_hash)
    fields[pair_start + 1] = json.dumps(old_hash)
    text = "{" + ",".join(fields) + "}"
    key = bytes(range(1, 21))
    plain = b"\xef\xbb\xbf" + text.encode("utf-8") + b"\x00"
    data = bytes([len(key)]) + key + bytes(value ^ key[index % len(key)] for index, value in enumerate(plain))

    new_data, details = adapter_server.infobase_user_password_data_clear(data)
    decoded = adapter_server.infobase_user_password_data_decode(new_data)

    assert len(new_data) == len(data)
    assert details["already_clear"] is False
    assert details["password_pair"] == [pair_start, pair_start + 1]
    assert decoded["values"][pair_start : pair_start + 2] == [
        adapter_server.INFOBASE_EMPTY_PASSWORD_SHA1_BASE64,
        adapter_server.INFOBASE_EMPTY_PASSWORD_SHA1_BASE64,
    ]
    assert decoded["values"][:pair_start] == adapter_server.infobase_user_password_data_decode(data)["values"][:pair_start]


def test_infobase_user_password_data_clear_is_idempotent() -> None:
    empty_hash = adapter_server.INFOBASE_EMPTY_PASSWORD_SHA1_BASE64
    fields = ["0"] * 38
    fields[7] = "1"
    fields[12] = json.dumps(empty_hash)
    fields[13] = json.dumps(empty_hash)
    text = "{" + ",".join(fields) + "}"
    key = b"test-password-key"
    plain = b"\xef\xbb\xbf" + text.encode("utf-8") + b"\x00"
    data = bytes([len(key)]) + key + bytes(value ^ key[index % len(key)] for index, value in enumerate(plain))

    new_data, details = adapter_server.infobase_user_password_data_clear(data)

    assert new_data == data
    assert details["already_clear"] is True


def test_infobase_user_password_data_set_writes_normal_and_uppercase_hashes() -> None:
    old_hash = base64.b64encode(hashlib.sha1(b"old").digest()).decode("ascii")
    fields = ["0"] * 39
    fields[7] = "1"
    fields[12] = json.dumps(old_hash)
    fields[13] = json.dumps(old_hash)
    text = "{" + ",".join(fields) + "}"
    key = b"set-password-key"
    plain = b"\xef\xbb\xbf" + text.encode("utf-8") + b"\x00"
    data = bytes([len(key)]) + key + bytes(value ^ key[index % len(key)] for index, value in enumerate(plain))

    new_data, details = adapter_server.infobase_user_password_data_set(data, "TestПароль")
    decoded = adapter_server.infobase_user_password_data_decode(new_data)
    expected = adapter_server.infobase_user_password_hash_pair("TestПароль")

    assert details["already_applied"] is False
    assert tuple(decoded["values"][12:14]) == expected
    assert expected[0] != expected[1]
    assert len(new_data) == len(data)


def test_infobase_user_password_clear_sql_commits_verified_single_row(monkeypatch: pytest.MonkeyPatch) -> None:
    platform_id = "a74272fec8cf262a405a568d7871fb07"
    old_hash = base64.b64encode(hashlib.sha1(b"old-test-password").digest()).decode("ascii")
    fields = ["0"] * 38
    fields[7] = "1"
    fields[12] = json.dumps(old_hash)
    fields[13] = json.dumps(old_hash)
    text = "{" + ",".join(fields) + "}"
    key = b"transaction-test-key"
    plain = b"\xef\xbb\xbf" + text.encode("utf-8") + b"\x00"
    old_data = bytes([len(key)]) + key + bytes(value ^ key[index % len(key)] for index, value in enumerate(plain))

    class FakeCursor:
        def __init__(self) -> None:
            self.mode = ""
            self.rowcount = -1
            self.new_data = b""

        def execute(self, query: str, params: tuple[object, ...]) -> None:
            normalized = " ".join(query.split()).casefold()
            if normalized.startswith("select id"):
                self.mode = "original"
            elif normalized.startswith("update dbo.v8users"):
                self.mode = "update"
                self.new_data = bytes(params[0])
                self.rowcount = 1
            elif normalized.startswith("select data"):
                self.mode = "readback"

        def fetchone(self) -> dict[str, object]:
            if self.mode == "original":
                return {
                    "ID": bytes.fromhex(platform_id),
                    "Name": "Аналитик",
                    "standard_authentication_enabled": 1,
                    "Data": old_data,
                }
            return {"Data": self.new_data}

    class FakeConnection:
        def __init__(self) -> None:
            self.cursor_instance = FakeCursor()
            self.committed = False
            self.rolled_back = False
            self.closed = False

        def cursor(self, *, as_dict: bool = False) -> FakeCursor:
            assert as_dict is True
            return self.cursor_instance

        def commit(self) -> None:
            self.committed = True

        def rollback(self) -> None:
            self.rolled_back = True

        def close(self) -> None:
            self.closed = True

    connection = FakeConnection()
    monkeypatch.setattr(
        adapter_server,
        "connect_live_sql",
        lambda base_id, method, *, timeout_seconds: (connection, {"database": "upo_test"}, None),
    )

    result = adapter_server.infobase_user_password_clear_sql(
        "upo_test",
        {"id": platform_id.upper(), "name": "Аналитик"},
        request_id="request-1",
        timeout_seconds=30,
    )

    assert result["status"] == "ok"
    assert result["applied"] is True
    assert result["source"]["kind"] == "live_sql"
    assert result["verification"]["only_current_password_hash_pair_changed"] is True
    assert connection.committed is True
    assert connection.rolled_back is False
    assert connection.closed is True


def test_infobase_user_password_write_history_never_stores_clear_text_secret(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    platform_id = "b58c9366f69fa7f54b687267775eb46c"
    secret = "must-never-enter-history"
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    monkeypatch.setenv("ONEC_INFOBASE_USER_ADMIN_ALLOW_UNAUTHENTICATED", "true")
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"server": "sql.example", "database": base_id}, None))
    monkeypatch.setattr(
        adapter_server,
        "infobase_user_password_change",
        lambda payload, *, operation: {
            "schema": "onec_infobase_user_password_change.v1",
            "status": "ok",
            "base_id": payload["base_id"],
            "operation": operation,
            "applied": True,
            "target": {"id": platform_id.upper(), "name": "Администратор 1С УПО 3"},
            "source": {"kind": "live_sql", "table": "dbo.v8users", "column": "Data"},
        },
    )

    result = adapter_server.call_method(
        "infobase.user.password.set",
        {
            "base_id": "upo",
            "user": "Администратор 1С УПО 3",
            "confirm_user_id": platform_id,
            "new_password": secret,
            "allow_password_change": True,
            "allow_administrator_password_change": True,
        },
    )

    assert result["operation_id"]
    history = adapter_server.call_method("metadata.write.history", {"base_id": "upo", "operation_id": result["operation_id"]})
    serialized = json.dumps(history, ensure_ascii=False)
    assert secret not in serialized
    assert history["operations"][0]["method"] == "infobase.user.password.set"
    assert history["operations"][0]["target_summary"]["target"] == {
        "id": platform_id.upper(),
        "name": "Администратор 1С УПО 3",
    }
    assert history["operations"][0]["target_summary"]["transport"] == "sql_dbo_v8users_data"


def test_access_user_explain_can_use_live_bsp_extract(monkeypatch: pytest.MonkeyPatch) -> None:
    def fake_extract(payload: dict[str, Any]) -> dict[str, Any]:
        assert payload["preset"] == "bsp"
        assert payload["limit"] == 20000
        assert payload["max_effective_permissions_per_user"] == 20000
        return {
            "status": "ok",
            "counts": {"effective_users": 1},
            "diagnostics": {"extractors": {"users": {"rows": 1}}},
            "graph": {
                "source": {"kind": "access_snapshot"},
                "effective_users": [
                    {
                        "user": {"id": "0000015C:u1", "name": "Иванов", "active": True, "service": False, "administrator": False},
                        "groups": ["g1"],
                        "roles": [{"id": "r1", "name": "Роль", "sources": []}],
                        "permissions": [{"object": "Catalog.Контрагенты", "object_name": "Контрагенты", "action": "Просмотр", "sources": []}],
                        "access_keys": {"counts": {"group_keys": 2, "user_keys": 1, "total": 3}, "samples": {"group_keys": [], "user_keys": []}},
                        "data_restrictions": [],
                    }
                ],
            },
        }

    monkeypatch.setattr(adapter_server, "access_snapshot_extract", fake_extract)

    result = call_method("access.user.explain", {"base_id": "upo_test", "user": "u1", "object": "Контрагенты"})

    assert result["status"] == "ok"
    assert result["source"] == {"kind": "live_sql", "preset": "bsp", "extraction": "access.snapshot.extract"}
    assert result["user"]["name"] == "Иванов"
    assert result["counts"]["access_keys"] == 3
    assert result["permissions"][0]["object_name"] == "Контрагенты"
    assert result["extraction_counts"] == {"effective_users": 1}


def test_access_snapshot_from_extractor_rows_builds_graph() -> None:
    rows_by_area = {
        "users": [{"UserRef": "u1", "UserName": "Иванов"}],
        "groups": [{"GroupRef": "g1", "GroupName": "Менеджеры"}],
        "profiles": [{"ProfileRef": "p1", "ProfileName": "Продажи"}],
        "roles": [{"RoleRef": "r1", "RoleName": "ЧтениеКонтрагентов"}],
        "group_users": [{"GroupRef": "g1", "UserRef": "u1"}],
        "group_profiles": [{"GroupRef": "g1", "ProfileRef": "p1"}],
        "profile_roles": [{"ProfileRef": "p1", "RoleRef": "r1"}],
        "role_permissions": [{"RoleRef": "r1", "ObjectRef": "Catalog.Контрагенты", "Action": "read"}],
    }
    mappings = {
        "users": {"id": "UserRef", "name": "UserName"},
        "groups": {"id": "GroupRef", "name": "GroupName"},
        "profiles": {"id": "ProfileRef", "name": "ProfileName"},
        "roles": {"id": "RoleRef", "name": "RoleName"},
        "group_users": {"group": "GroupRef", "user": "UserRef"},
        "group_profiles": {"group": "GroupRef", "profile": "ProfileRef"},
        "profile_roles": {"profile": "ProfileRef", "role": "RoleRef"},
        "role_permissions": {"role": "RoleRef", "object": "ObjectRef", "action": "Action"},
    }

    access = adapter_server.access_snapshot_from_extractor_rows(rows_by_area, mappings)
    graph = adapter_server.build_access_graph_from_snapshot(access, base_id="upo_test")

    assert access["groups"][0]["users"] == ["u1"]
    assert access["profiles"][0]["roles"] == ["r1"]
    assert graph["effective_users"][0]["permissions"][0]["object"] == "Catalog.Контрагенты"
    assert graph["effective_users"][0]["permissions"][0]["sources"][0]["chains"][0]["group"] == "g1"


def test_access_snapshot_expands_user_group_members() -> None:
    rows_by_area = {
        "users": [{"id": "u1", "name": "Иванов"}, {"id": "ug1", "name": "Отдел продаж"}],
        "groups": [{"id": "g1", "name": "Доступ продажи"}],
        "profiles": [{"id": "p1", "name": "Продажи"}],
        "roles": [{"id": "r1", "name": "Чтение"}],
        "group_users": [{"group": "g1", "user": "ug1"}],
        "user_group_members": [{"group": "ug1", "user": "u1"}],
        "group_profiles": [{"group": "g1", "profile": "p1"}],
        "profile_roles": [{"profile": "p1", "role": "r1"}],
        "role_permissions": [{"role": "r1", "object": "Catalog.Контрагенты", "action": "read"}],
    }

    access = adapter_server.access_snapshot_from_extractor_rows(rows_by_area, {})
    graph = adapter_server.build_access_graph_from_snapshot(access, base_id="upo_test")
    users = {item["user"]["id"]: item for item in graph["effective_users"]}

    assert "u1" in users
    assert "g1" in users["u1"]["groups"]
    assert users["u1"]["permissions"][0]["object"] == "Catalog.Контрагенты"


def test_access_role_permission_actions_split_into_effective_permissions() -> None:
    rows_by_area = {
        "users": [{"id": "u1", "name": "Иванов"}],
        "groups": [{"id": "g1", "name": "Доступ продажи"}],
        "profiles": [{"id": "p1", "name": "Продажи"}],
        "roles": [{"id": "r1", "name": "Роль"}],
        "group_users": [{"group": "g1", "user": "u1"}],
        "group_profiles": [{"group": "g1", "profile": "p1"}],
        "profile_roles": [{"profile": "p1", "role": "r1"}],
        "role_permissions": [{"role": "r1", "object": "Document.ЗаказКлиента", "actions": "Добавление,Изменение,", "source_fields": "_Fld27940,_Fld27941,"}],
    }

    access = adapter_server.access_snapshot_from_extractor_rows(rows_by_area, {})
    graph = adapter_server.build_access_graph_from_snapshot(access, base_id="upo_test")
    permissions = graph["effective_users"][0]["permissions"]

    assert {permission["action"] for permission in permissions} == {"Добавление", "Изменение"}
    assert permissions[0]["sources"][0]["role"] == "r1"


def test_access_graph_limits_effective_permissions_per_user() -> None:
    access = {
        "users": [{"id": "u1", "name": "Иванов", "roles": ["r1"]}],
        "roles": [
            {
                "id": "r1",
                "permissions": [
                    {"object": "Catalog.A", "action": "read"},
                    {"object": "Catalog.B", "action": "read"},
                    {"object": "Catalog.C", "action": "read"},
                ],
            }
        ],
    }

    graph = adapter_server.build_access_graph_from_snapshot(access, base_id="upo_test", max_permissions_per_user=2)
    user = graph["effective_users"][0]

    assert len(user["permissions"]) == 2
    assert user["permission_counts"] == {"total": 3, "returned": 2, "truncated": True}
    assert graph["counts"]["effective_permissions_total"] == 3
    assert graph["counts"]["effective_permissions_returned"] == 2
    assert graph["counts"]["effective_users_permissions_truncated"] == 1


def test_access_graph_enriches_resolved_access_identifiers(monkeypatch: pytest.MonkeyPatch) -> None:
    role_id = "000000C4:9FAE0050569DB20C11ED22BB38CEA4C6"
    object_id = "000000C4:9FAE0050569DB20C11ED22BB38CEA62D"

    def fake_resolve(config: dict[str, str] | None, value: str) -> dict[str, Any] | None:
        if value == role_id:
            return {"guid": "38cea4c6-22bb-11ed-9fae-0050569db20c", "kind": "Role", "name": "МенеджерПродаж"}
        if value == object_id:
            return {
                "guid": "38cea62d-22bb-11ed-9fae-0050569db20c",
                "kind": "Catalog",
                "name": "Контрагенты",
                "full_name": "Справочник.Контрагенты",
            }
        return None

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"server": "s", "database": "d"}, None))
    monkeypatch.setattr(adapter_server, "access_identifier_payload_from_index", fake_resolve)

    graph = adapter_server.build_access_graph_from_snapshot(
        {
            "users": [{"id": "u1", "name": "User"}],
            "roles": [{"id": role_id, "permissions": [{"object": object_id, "action": "Просмотр"}]}],
            "assignments": [{"subject": "u1", "roles": [role_id]}],
        },
        base_id="upo_test",
        resolve_identifiers=True,
    )

    role = graph["effective_users"][0]["roles"][0]
    permission = graph["effective_users"][0]["permissions"][0]
    assert role["name"] == "МенеджерПродаж"
    assert permission["object_name"] == "Контрагенты"
    assert permission["object_full_name"] == "Справочник.Контрагенты"
    assert graph["diagnostics"]["identifier_resolution"]["roles"]["resolved"] == 1
    assert graph["diagnostics"]["identifier_resolution"]["objects"]["resolved"] == 1


def test_access_schema_candidate_score_finds_access_tables() -> None:
    score, reasons = adapter_server.access_schema_candidate_score(
        "_Reference.ПрофилиГруппДоступа",
        ["_IDRRef", "_Description", "Роль", "ПравоДоступа"],
    )

    assert score > 0
    assert {"profiles", "roles", "permissions"}.issubset(set(reasons))


def test_access_bsp_extractor_plan_builds_expected_vt_tables(monkeypatch: pytest.MonkeyPatch) -> None:
    def profile(sql_number: int, sections: list[dict[str, Any]] | None = None) -> dict[str, Any]:
        return {
            "status": "ok",
            "object": {"storage": {"dbnames": [{"sql_number": sql_number}]}},
            "tabular_sections": sections or [],
        }

    fake_profiles = {
        ("Catalog", "Пользователи"): profile(348),
        ("Catalog", "ВнешниеПользователи"): profile(141),
        ("Catalog", "ГруппыДоступа"): profile(
            146,
            [{"name": "Пользователи", "columns": [{"name": "Пользователь", "storage_routes": [{"sql_number": 14130}]}]}],
        ),
        ("Catalog", "ГруппыПользователей"): profile(
            149,
            [{"name": "Состав", "columns": [{"name": "Пользователь", "storage_routes": [{"sql_number": 14151}]}]}],
        ),
        ("Catalog", "ПрофилиГруппДоступа"): profile(
            376,
            [{"name": "Роли", "columns": [{"name": "Роль", "storage_routes": [{"sql_number": 19417}]}]}],
        ),
        ("Catalog", "ИдентификаторыОбъектовМетаданных"): profile(196),
        ("Catalog", "КлючиДоступа"): profile(226),
        ("Catalog", "НаборыГруппДоступа"): profile(259),
        ("InformationRegister", "ПраваРолей"): profile(27937),
        ("InformationRegister", "КлючиДоступаГруппДоступа"): profile(25996),
        ("InformationRegister", "КлючиДоступаПользователей"): profile(26025),
        ("InformationRegister", "КлючиДоступаКОбъектам"): profile(26003),
        ("InformationRegister", "КлючиДоступаНаборовГруппДоступа"): profile(26018),
    }

    monkeypatch.setattr(adapter_server, "metadata_object_attributes", lambda payload: fake_profiles[(payload["kind"], payload["name"])])

    def fake_existing_tables(base_id: str, table_prefix: str, timeout_seconds: int = 30) -> list[str]:
        if table_prefix == "_Reference348":
            return ["_Reference348", "_Reference348X1"]
        if table_prefix == "_Reference196":
            return ["_Reference196X1"]
        return [table_prefix]

    monkeypatch.setattr(adapter_server, "access_sql_existing_tables", fake_existing_tables)

    queries, mappings, diagnostics = adapter_server.access_bsp_extractor_plan("upo_test")

    assert queries is not None
    assert mappings is not None
    tables = diagnostics["metadata"]["tables"]
    assert tables["group_users"] == "_Reference146_VT14128"
    assert tables["profile_roles"] == "_Reference376_VT19415"
    assert tables["user_group_members"] == "_Reference149_VT14149"
    assert tables["role_permissions"] == "_InfoRg27937"
    assert tables["access_group_keys"] == "_InfoRg25996"
    assert tables["access_user_keys"] == "_InfoRg26025"
    assert tables["access_object_keys"] == "_InfoRg26003"
    assert "FROM dbo.[_Reference376_VT19415]" in queries["profile_roles"]
    assert "FROM dbo.[_Reference348X1]" in queries["users"]
    assert "AS marked" in queries["users"]
    assert "AS user_type" in queries["users"]
    assert "AS service" in queries["users"]
    assert mappings["users"]["marked"] == "marked"
    assert mappings["users"]["user_type"] == "user_type"
    assert "FROM dbo.[_InfoRg27937]" in queries["role_permissions"]
    assert "LEFT JOIN (SELECT _IDRRef, _Description FROM dbo.[_Reference196X1]" in queries["role_permissions"]
    assert "FROM dbo.[_InfoRg25996]" in queries["access_group_keys"]
    assert "AS group_ref" in queries["access_group_keys"]
    assert "AS [user]" in queries["access_user_keys"]
    assert "AS object_sql_number" in queries["access_object_keys"]
    assert "AS object_id" in queries["access_object_keys"]
    assert mappings["role_permissions"]["source_fields"] == "source_fields"
    assert mappings["role_permissions"]["object_name"] == "object_name"
    assert mappings["access_group_keys"]["access_key"] == "access_key"
    assert mappings["access_user_keys"]["user"] == "user"
    assert "Добавление" in queries["role_permissions"]
    assert tables["metadata_identifiers"] == ["_Reference196X1"]
    assert diagnostics["metadata"]["permission_action_labels"]["mapping"]["_Fld27940"] == "Добавление"


def test_access_snapshot_extract_defaults_to_bsp_preset(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "access_bsp_extractor_plan",
        lambda base_id: (
            {"users": "SELECT N'u1' AS id, N'Иванов' AS name"},
            {"users": {"id": "id", "name": "name"}},
            {"preset": "bsp"},
        ),
    )
    monkeypatch.setattr(
        adapter_server,
        "access_rows_from_query",
        lambda base_id, query, *, limit, timeout_seconds: ([{"id": "u1", "name": "Иванов"}], None, False),
    )
    monkeypatch.setattr(
        adapter_server,
        "build_access_graph_from_snapshot",
        lambda access, **kwargs: {"counts": {"users": len(access.get("users") or [])}, "effective_users": []},
    )

    result = adapter_server.access_snapshot_extract({"base_id": "upo", "limit": 10})

    assert result["status"] == "ok"
    assert result["preset"] == "bsp"
    assert result["counts"]["users"] == 1


def test_access_keys_query_filters_group_keys(monkeypatch: pytest.MonkeyPatch) -> None:
    captured: dict[str, Any] = {}

    monkeypatch.setattr(
        adapter_server,
        "access_bsp_extractor_plan",
        lambda base_id: (
            {"access_group_keys": "SELECT N'g1' AS [group], N'Продажи' AS group_name, N'k1' AS access_key"},
            {"access_group_keys": {"group": "group", "group_name": "group_name", "access_key": "access_key"}},
            {"preset": "bsp"},
        ),
    )

    def fake_rows(base_id: str, query: str, *, limit: int, timeout_seconds: int) -> tuple[list[dict[str, Any]], None, bool]:
        captured["query"] = query
        captured["limit"] = limit
        return [{"group": "g1", "group_name": "Продажи", "access_key": "k1"}], None, False

    monkeypatch.setattr(adapter_server, "access_rows_from_query", fake_rows)

    result = adapter_server.access_keys_query({"base_id": "upo_test", "kind": "group", "group": "g1", "limit": 10, "offset": 20})

    assert result["status"] == "ok"
    assert result["areas"]["access_group_keys"]["rows"] == [{"group": "g1", "group_name": "Продажи", "access_key": "k1"}]
    assert "q.[group] = N'g1'" in captured["query"]
    assert "OFFSET 20 ROWS FETCH NEXT 11 ROWS ONLY" in captured["query"]
    assert captured["limit"] == 11


def test_access_object_record_filters_resolve_public_names_without_consuming_query_kind(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_schema(payload: dict[str, Any]) -> dict[str, Any]:
        seen["schema_payload"] = payload
        return {
            "status": "ok",
            "object": {
                "kind": "Catalog",
                "name": "Номенклатура",
                "guid": "11111111-1111-1111-1111-111111111111",
            },
            "table": {"name": "_Reference106", "row_kind": "Catalog"},
        }

    monkeypatch.setattr(adapter_server, "data_object_schema", fake_schema)

    normalized, resolution, error = adapter_server.normalize_access_object_record_filters(
        {
            "base_id": "upo_test",
            "kind": "object",
            "object_ref": "Справочники.Номенклатура",
            "record_ref": "00112233-4455-6677-8899-aabbccddeeff",
        },
        "access.keys.query",
        "upo_test",
        timeout_seconds=60,
    )

    assert error is None
    assert normalized["kind"] == "object"
    assert normalized["object_sql_number"] == 106
    assert normalized["object_id"] == "00112233445566778899AABBCCDDEEFF"
    assert seen["schema_payload"]["object_ref"] == "Справочники.Номенклатура"
    assert "kind" not in seen["schema_payload"]
    assert resolution["metadata_object"]["ref"] == "Catalog.Номенклатура"
    assert resolution["data_record"]["record_ref"] == "00112233445566778899AABBCCDDEEFF"


def test_access_keys_query_maps_object_ref_and_record_ref_to_bsp_filters(monkeypatch: pytest.MonkeyPatch) -> None:
    captured: dict[str, Any] = {}
    record_ref = "00112233445566778899AABBCCDDEEFF"

    monkeypatch.setattr(
        adapter_server,
        "data_object_schema",
        lambda payload: {
            "status": "ok",
            "object": {"kind": "Catalog", "name": "Номенклатура"},
            "table": {"name": "_Reference106"},
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "access_bsp_extractor_plan",
        lambda base_id: (
            {
                "access_object_keys": (
                    "SELECT N'x' AS object, N'0000006A' AS object_type_code, "
                    "106 AS object_sql_number, N'x' AS object_id, N'k1' AS access_key"
                )
            },
            {
                "access_object_keys": {
                    "object": "object",
                    "object_type_code": "object_type_code",
                    "object_sql_number": "object_sql_number",
                    "object_id": "object_id",
                    "access_key": "access_key",
                }
            },
            {"preset": "bsp"},
        ),
    )

    def fake_rows(base_id: str, query: str, *, limit: int, timeout_seconds: int) -> tuple[list[dict[str, Any]], None, bool]:
        captured["query"] = query
        return [], None, False

    monkeypatch.setattr(adapter_server, "access_rows_from_query", fake_rows)

    result = adapter_server.access_keys_query(
        {
            "base_id": "upo_test",
            "kind": "object",
            "object_ref": "Справочники.Номенклатура",
            "record_ref": record_ref,
        }
    )

    assert result["status"] == "ok"
    assert "q.[object_sql_number] = 106" in captured["query"]
    assert f"q.[object_id] = N'{record_ref}'" in captured["query"]
    assert result["selector_resolution"]["metadata_object"]["name"] == "Номенклатура"
    assert result["areas"]["access_object_keys"]["filters"]["object_ref"] == "Справочники.Номенклатура"
    assert result["areas"]["access_object_keys"]["filters"]["record_ref"] == record_ref


def test_access_object_record_filters_reject_conflicting_legacy_id(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "data_object_schema",
        lambda payload: {
            "status": "ok",
            "object": {"kind": "Document", "name": "ЗаказКлиента"},
            "table": {"name": "_Document422"},
        },
    )

    _, _, error = adapter_server.normalize_access_object_record_filters(
        {
            "object_ref": "Документы.ЗаказКлиента",
            "object_sql_number": 421,
        },
        "access.object_keys.resolve",
        "upo_test",
        timeout_seconds=60,
    )

    assert error is not None
    assert error["status"] == "invalid_argument"
    assert error["argument"] == "object_sql_number"


def test_access_key_help_and_mcp_schemas_distinguish_object_and_record_refs() -> None:
    methods = {row["name"]: row for row in adapter_server.call_method("help.methods", {}).get("methods") or []}
    mcp_tools = {row["name"]: row for row in adapter_mcp.TOOLS}

    for method in ("access.keys.query", "access.object_keys.resolve", "access.object.explain"):
        properties = methods[method]["input_schema"]["properties"]
        assert "object_ref" in properties
        assert "record_ref" in properties
    assert "not a 1C metadata kind" in methods["access.keys.query"]["input_schema"]["properties"]["kind"]["description"]
    for tool in ("access_keys_query", "access_object_keys_resolve", "access_object_explain"):
        properties = mcp_tools[tool]["inputSchema"]["properties"]
        assert "object_ref" in properties
        assert "record_ref" in properties


def test_access_object_roles_resolves_name_to_guid_and_filters_roles(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "resolve_object_guid",
        lambda payload, base_id, **kwargs: (
            "38cea62d-22bb-11ed-9fae-0050569db20c",
            "InformationRegister",
            {
                "guid": "38cea62d-22bb-11ed-9fae-0050569db20c",
                "kind": "InformationRegister",
                "kind_ru": "Регистр сведений",
                "name": "УОП_АктуальныеСпецификации",
                "synonym": "Актуальные рецептуры",
            },
            None,
        ),
    )
    monkeypatch.setattr(adapter_server, "access_object_roles_fast_permissions", lambda *args, **kwargs: {"status": "error"})
    monkeypatch.setattr(
        adapter_server,
        "access_snapshot_extract",
        lambda payload: {
            "status": "ok",
            "counts": {"roles": 3},
            "diagnostics": {"preset": "bsp"},
            "access": {
                "roles": [
                    {
                        "id": "r1",
                        "name": "Чтение рецептур",
                        "permissions": [
                            {
                                "object": "000000C4:AAA",
                                "action": "Просмотр",
                                "object_name": "Актуальные рецептуры (Регистр сведений)",
                            }
                        ],
                    },
                    {
                        "id": "r2",
                        "name": "Изменение рецептур",
                        "permissions": [
                            {
                                "object": "000000C4:BBB",
                                "actions": "Добавление,ИзменениеБезОграничения,",
                                "object_name": "Актуальные рецептуры (Регистр сведений)",
                            }
                        ],
                    },
                    {
                        "id": "r3",
                        "name": "Другая роль",
                        "permissions": [
                            {
                                "object": "000000C4:CCC",
                                "action": "Просмотр",
                                "object_name": "Другой регистр (Регистр сведений)",
                                "object_resolution": {
                                    "guid": "11111111-1111-1111-1111-111111111111",
                                    "kind": "InformationRegister",
                                    "kind_ru": "Регистр сведений",
                                    "name": "ДругойРегистр",
                                    "full_name": "InformationRegister.ДругойРегистр",
                                },
                            }
                        ],
                    },
                ]
            },
        },
    )

    result = adapter_server.access_object_roles({"base_id": "upo_test", "ref": "РегистрСведений.УОП_АктуальныеСпецификации"})

    assert result["status"] == "ok"
    assert result["object"]["guid"] == "38cea62d-22bb-11ed-9fae-0050569db20c"
    assert [item["id"] for item in result["roles"]] == ["r2", "r1"]
    assert result["roles"][0]["rights"] == {"read": False, "insert": True, "update": True, "delete": False}
    assert result["roles"][0]["rights_detail"]["insert"] == {"allowed": True, "unrestricted": False}
    assert result["roles"][0]["rights_detail"]["update"] == {"allowed": True, "unrestricted": True}
    assert result["roles"][1]["rights"] == {"read": True, "insert": False, "update": False, "delete": False}
    assert result["roles"][1]["rights_detail"]["read"] == {"allowed": True, "unrestricted": False}


def test_access_object_roles_filters_action(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "resolve_object_guid",
        lambda payload, base_id, **kwargs: (
            "38cea62d-22bb-11ed-9fae-0050569db20c",
            "InformationRegister",
            {"guid": "38cea62d-22bb-11ed-9fae-0050569db20c", "kind": "InformationRegister", "name": "УОП_АктуальныеСпецификации"},
            None,
        ),
    )
    monkeypatch.setattr(adapter_server, "access_object_roles_fast_permissions", lambda *args, **kwargs: {"status": "error"})
    monkeypatch.setattr(
        adapter_server,
        "access_snapshot_extract",
        lambda payload: {
            "status": "ok",
            "counts": {"roles": 2},
            "diagnostics": {"preset": "bsp"},
            "access": {
                "roles": [
                    {
                        "id": "r1",
                        "name": "Чтение рецептур",
                        "permissions": [
                            {
                                "object": "000000C4:AAA",
                                "action": "Просмотр",
                                "object_resolution": {"guid": "38cea62d-22bb-11ed-9fae-0050569db20c", "kind_ru": "РегистрСведений", "name": "УОП_АктуальныеСпецификации"},
                            }
                        ],
                    },
                    {
                        "id": "r2",
                        "name": "Изменение рецептур",
                        "permissions": [
                            {
                                "object": "000000C4:BBB",
                                "action": "Изменение",
                                "object_resolution": {"guid": "38cea62d-22bb-11ed-9fae-0050569db20c", "kind_ru": "РегистрСведений", "name": "УОП_АктуальныеСпецификации"},
                            }
                        ],
                    },
                ]
            },
        },
    )

    result = adapter_server.access_object_roles(
        {"base_id": "upo_test", "kind": "InformationRegister", "name": "УОП_АктуальныеСпецификации", "action": "read"}
    )

    assert result["status"] == "ok"
    assert [item["id"] for item in result["roles"]] == ["r1"]
    assert result["roles"][0]["rights"]["read"] is True


def test_access_object_roles_fast_permissions_filters_sql_rows(monkeypatch: pytest.MonkeyPatch) -> None:
    captured: dict[str, object] = {}

    monkeypatch.setattr(
        adapter_server,
        "access_bsp_extractor_plan",
        lambda base_id: (
            {"role_permissions": "SELECT role, object, role_name, object_name, actions, source_fields FROM rights"},
            {"role_permissions": {"role": "role", "object": "object", "role_name": "role_name", "object_name": "object_name", "actions": "actions", "source_fields": "source_fields"}},
            {"preset": "bsp"},
        ),
    )

    def fake_rows(base_id: str, query: str, *, limit: int, timeout_seconds: int) -> tuple[list[dict[str, object]], None, bool]:
        captured["query"] = query
        return (
            [
                {
                    "role": "r1",
                    "object": "000000C4:AAA",
                    "role_name": "УПО. Добавление изменение актуальных спецификаций (Роль)",
                    "object_name": "Актуальные рецептуры (Регистр сведений)",
                    "actions": "Добавление,ИзменениеБезОграничения,",
                    "source_fields": "_Fld27940,_Fld27943,",
                },
                {
                    "role": "r2",
                    "object": "000000C4:BBB",
                    "role_name": "Другая роль",
                    "object_name": "Другой регистр (Регистр сведений)",
                    "actions": "ИзменениеБезОграничения,",
                    "source_fields": "_Fld27943,",
                },
            ],
            None,
            False,
        )

    monkeypatch.setattr(adapter_server, "access_rows_from_query", fake_rows)

    result = adapter_server.access_object_roles_fast_permissions(
        "upo_test",
        object_payload={"name": "УОП_АктуальныеСпецификации", "synonym": "Актуальные рецептуры"},
        object_selector="InformationRegister.УОП_АктуальныеСпецификации",
        object_guid="38cea62d-22bb-11ed-9fae-0050569db20c",
        action_filter_keys={"insert", "update"},
        timeout_seconds=30,
        limit=10,
    )

    assert result["status"] == "ok"
    assert "Актуальные рецептуры" in str(captured["query"])
    assert [item["name"] for item in result["roles"]] == ["УПО. Добавление изменение актуальных спецификаций (Роль)"]
    assert result["roles"][0]["rights"] == {"read": False, "insert": True, "update": True, "delete": False}


def test_access_object_roles_fast_permissions_does_not_treat_read_flags_as_write(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "access_bsp_extractor_plan",
        lambda base_id: (
            {"role_permissions": "SELECT role, object, role_name, object_name, actions, source_fields FROM rights"},
            {"role_permissions": {"role": "role", "object": "object", "role_name": "role_name", "object_name": "object_name", "actions": "actions", "source_fields": "source_fields"}},
            {"preset": "bsp"},
        ),
    )

    def fake_rows(base_id: str, query: str, *, limit: int, timeout_seconds: int) -> tuple[list[dict[str, object]], None, bool]:
        return (
            [
                {
                    "role": "r1",
                    "object": "000000C4:AAA",
                    "role_name": "УПО. Чтение актуальных спецификаций (Роль)",
                    "object_name": "Актуальные рецептуры (Регистр сведений)",
                    "actions": "Чтение,ЧтениеБезОграничения,",
                    "source_fields": "_Fld27942,_Fld27945,",
                }
            ],
            None,
            False,
        )

    monkeypatch.setattr(adapter_server, "access_rows_from_query", fake_rows)

    write_result = adapter_server.access_object_roles_fast_permissions(
        "upo_test",
        object_payload={"name": "УОП_АктуальныеСпецификации", "synonym": "Актуальные рецептуры"},
        object_selector="InformationRegister.УОП_АктуальныеСпецификации",
        object_guid="38cea62d-22bb-11ed-9fae-0050569db20c",
        action_filter_keys={"insert", "update"},
        timeout_seconds=30,
        limit=10,
    )
    read_result = adapter_server.access_object_roles_fast_permissions(
        "upo_test",
        object_payload={"name": "УОП_АктуальныеСпецификации", "synonym": "Актуальные рецептуры"},
        object_selector="InformationRegister.УОП_АктуальныеСпецификации",
        object_guid="38cea62d-22bb-11ed-9fae-0050569db20c",
        action_filter_keys={"read"},
        timeout_seconds=30,
        limit=10,
    )

    assert write_result["status"] == "not_found"
    assert read_result["status"] == "ok"
    assert read_result["roles"][0]["rights"] == {"read": True, "insert": False, "update": False, "delete": False}
    assert read_result["roles"][0]["rights_detail"]["read"] == {"allowed": True, "unrestricted": True}


def test_access_object_subjects_fast_chain_returns_profiles_groups_users(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "access_bsp_extractor_plan",
        lambda base_id: (
            {
                "profiles": "profiles",
                "profile_roles": "profile_roles",
                "group_profiles": "group_profiles",
                "groups": "groups",
                "group_users": "group_users",
                "user_group_members": "user_group_members",
                "users": "users",
            },
            {
                "profiles": {"id": "id", "name": "name"},
                "profile_roles": {"profile": "profile", "role": "role"},
                "group_profiles": {"group": "group", "profile": "profile"},
                "groups": {"id": "id", "name": "name", "roles": "roles"},
                "group_users": {"group": "group", "user": "user"},
                "user_group_members": {"group": "group", "user": "user"},
                "users": {"id": "id", "name": "name", "active": "active", "marked": "marked", "user_type": "user_type", "roles": "roles"},
            },
            {"preset": "bsp"},
        ),
    )

    rows_by_query = {
        "profiles": [{"id": "p1", "name": "Технологи"}],
        "profile_roles": [{"profile": "p1", "role": "r1"}],
        "group_profiles": [{"group": "g1", "profile": "p1"}],
        "groups": [{"id": "g1", "name": "Технолог"}, {"id": "g2", "name": "Прямая группа", "roles": ["r1"]}],
        "group_users": [{"group": "g1", "user": "u1"}, {"group": "g2", "user": "u2"}],
        "user_group_members": [],
        "users": [
            {"id": "u1", "name": "Ivan.PETROV", "active": True, "marked": False, "user_type": "user"},
            {"id": "u2", "name": "Anna.SIDOROVA", "active": True, "marked": False, "user_type": "user"},
            {"id": "u3", "name": "Direct.USER", "active": True, "marked": False, "user_type": "user", "roles": ["r1"]},
        ],
    }

    monkeypatch.setattr(
        adapter_server,
        "access_rows_from_query",
        lambda base_id, query, *, limit, timeout_seconds: (rows_by_query[str(query)], None, False),
    )

    result = adapter_server.access_object_subjects_fast_chain(
        "upo_test",
        matched_roles=[{"id": "r1", "name": "УПО. Добавление изменение актуальных спецификаций (Роль)"}],
        timeout_seconds=30,
        limit=2,
        scope_subject_limit=10,
    )

    assert result["status"] == "ok"
    assert result["counts"] == {"roles": 1, "profiles": 1, "groups": 2, "users": 3}
    assert [profile["name"] for profile in result["profiles"]] == ["Технологи"]
    assert [group["name"] for group in result["groups"]] == ["Прямая группа", "Технолог"]
    assert [user["name"] for user in result["users"]] == ["Anna.SIDOROVA", "Direct.USER"]
    assert [user["name"] for user in result["_scope_users"]] == ["Anna.SIDOROVA", "Direct.USER", "Ivan.PETROV"]
    assert result["users"][0]["role_sources"][0]["type"] == "group_role"
    assert result["users"][1]["role_sources"][0]["type"] == "direct_user_role"
    assert result["_scope_users"][2]["role_sources"][0]["profile_name"] == "Технологи"


def test_access_subject_access_key_scope_filters_subject_keys(monkeypatch: pytest.MonkeyPatch) -> None:
    captured_queries: list[str] = []
    monkeypatch.setattr(
        adapter_server,
        "access_bsp_extractor_plan",
        lambda base_id: (
            {"access_group_keys": "group_keys", "access_user_keys": "user_keys"},
            {
                "access_group_keys": {"group": "group", "group_ref": "group_ref", "group_name": "group_name", "access_key": "access_key"},
                "access_user_keys": {"user_set": "user_set", "user_set_name": "user_set_name", "user": "user", "access_key": "access_key"},
            },
            {"preset": "bsp"},
        ),
    )

    def fake_rows(base_id: str, query: str, *, limit: int, timeout_seconds: int) -> tuple[list[dict[str, object]], None, bool]:
        captured_queries.append(query)
        if query.startswith("SELECT * FROM (group_keys)"):
            return ([{"group": "g1", "group_ref": "00000092:g1", "group_name": "Технолог", "access_key": "kg1"}], None, False)
        if query.startswith("SELECT * FROM (user_keys)"):
            return ([{"user": "0000008D:u1", "user_set": "s1", "user_set_name": "Ivan.PETROV", "access_key": "ku1"}], None, False)
        return ([], None, False)

    monkeypatch.setattr(adapter_server, "access_rows_from_query", fake_rows)

    result = adapter_server.access_subject_access_key_scope(
        "upo_test",
        groups=[{"id": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "name": "Технолог"}],
        users=[{"id": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "name": "Ivan.PETROV"}],
        total_groups=3,
        total_users=5,
        timeout_seconds=30,
    )

    assert result["status"] == "ok"
    assert result["counts"]["group_key_rows"] == 1
    assert result["counts"]["user_key_rows"] == 1
    assert result["counts"]["subject_access_keys"] == 2
    assert result["coverage"] == {"groups_checked": 1, "groups_total": 3, "users_checked": 1, "users_total": 5, "complete": False}
    assert "not metadata-object role permissions" in result["note"]
    assert any("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" in query for query in captured_queries)
    assert any("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" in query for query in captured_queries)


def test_access_object_subjects_include_access_key_scope_switch(monkeypatch: pytest.MonkeyPatch) -> None:
    scope_calls = 0
    scope_kwargs: dict[str, object] = {}
    object_guid = "38cea62d-22bb-11ed-9fae-0050569db20c"
    monkeypatch.setattr(
        adapter_server,
        "resolve_object_guid",
        lambda payload, base_id, **kwargs: (
            object_guid,
            "InformationRegister",
            {"guid": object_guid, "kind": "InformationRegister", "name": "УОП_АктуальныеСпецификации"},
            None,
        ),
    )
    monkeypatch.setattr(
        adapter_server,
        "access_object_roles_fast_permissions",
        lambda *args, **kwargs: {
            "status": "ok",
            "roles": [{"id": "r1", "name": "УПО. Добавление изменение актуальных спецификаций (Роль)", "permissions": []}],
            "counts": {"roles": 1, "permissions": 1},
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "access_object_subjects_fast_chain",
        lambda *args, **kwargs: {
            "status": "ok",
            "roles": [{"id": "r1", "name": "УПО. Добавление изменение актуальных спецификаций (Роль)", "permissions": []}],
            "profiles": [{"id": "p1", "name": "Технологи"}],
            "groups": [{"id": "g1", "name": "Технолог"}],
            "users": [{"id": "u1", "name": "Ivan.PETROV"}],
            "_scope_groups": [{"id": "g1", "name": "Технолог"}, {"id": "g2", "name": "Производство"}],
            "_scope_users": [{"id": "u1", "name": "Ivan.PETROV"}, {"id": "u2", "name": "Anna.SIDOROVA"}],
            "counts": {"roles": 1, "profiles": 1, "groups": 2, "users": 2},
        },
    )

    def fake_scope(*args: object, **kwargs: object) -> dict[str, object]:
        nonlocal scope_calls
        scope_calls += 1
        scope_kwargs.update(kwargs)
        return {"status": "ok", "counts": {"subject_access_keys": 2}, "coverage": {"complete": True}, "truncated": False}

    monkeypatch.setattr(adapter_server, "access_subject_access_key_scope", fake_scope)

    fast = adapter_server.access_object_subjects(
        {"base_id": "upo_test", "ref": "РегистрСведений.УОП_АктуальныеСпецификации", "action": "write"}
    )
    extended = adapter_server.access_object_subjects(
        {
            "base_id": "upo_test",
            "ref": "РегистрСведений.УОП_АктуальныеСпецификации",
            "action": "write",
            "limit": 1,
            "include_access_key_scope": True,
            "access_key_scope_limit": 123,
            "access_key_scope_subject_limit": 10,
        }
    )

    assert "access_key_scope" not in fast
    assert extended["access_key_scope"]["counts"]["subject_access_keys"] == 2
    assert scope_calls == 1
    assert [group["id"] for group in scope_kwargs["groups"]] == ["g1", "g2"]
    assert [user["id"] for user in scope_kwargs["users"]] == ["u1", "u2"]
    assert scope_kwargs["total_groups"] == 2
    assert scope_kwargs["total_users"] == 2
    assert scope_kwargs["limit"] == 123


def test_access_object_subjects_returns_roles_profiles_groups_users(monkeypatch: pytest.MonkeyPatch) -> None:
    object_guid = "38cea62d-22bb-11ed-9fae-0050569db20c"
    monkeypatch.setattr(
        adapter_server,
        "resolve_object_guid",
        lambda payload, base_id, **kwargs: (
            object_guid,
            "InformationRegister",
            {"guid": object_guid, "kind": "InformationRegister", "name": "УОП_АктуальныеСпецификации"},
            None,
        ),
    )
    monkeypatch.setattr(adapter_server, "access_object_roles_fast_permissions", lambda *args, **kwargs: {"status": "error"})
    monkeypatch.setattr(
        adapter_server,
        "access_snapshot_extract",
        lambda payload: {
            "status": "ok",
            "counts": {"roles": 2, "effective_users": 2},
            "diagnostics": {"preset": "bsp"},
            "access": {
                "roles": [
                    {
                        "id": "r1",
                        "name": "Чтение рецептур",
                        "permissions": [
                            {
                                "object": "000000C4:AAA",
                                "action": "Просмотр",
                                "object_resolution": {"guid": object_guid, "kind": "InformationRegister", "name": "УОП_АктуальныеСпецификации"},
                            }
                        ],
                    },
                    {
                        "id": "r2",
                        "name": "Изменение рецептур",
                        "permissions": [
                            {
                                "object": "000000C4:AAA",
                                "action": "ИзменениеБезОграничения",
                                "object_resolution": {"guid": object_guid, "kind": "InformationRegister", "name": "УОП_АктуальныеСпецификации"},
                            }
                        ],
                    },
                ],
                "profiles": [{"id": "p1", "name": "Технологи", "roles": ["r2"]}],
                "groups": [{"id": "g1", "name": "Технолог", "profiles": ["p1"], "users": ["u1"]}],
            },
            "graph": {
                "effective_users": [
                    {
                        "user": {"id": "u1", "name": "Ivan.PETROV", "active": True},
                        "groups": ["g1"],
                        "roles": [{"id": "r2", "name": "Изменение рецептур"}],
                        "permissions": [
                            {
                                "object": "000000C4:AAA",
                                "action": "ИзменениеБезОграничения",
                                "object_resolution": {"guid": object_guid, "kind": "InformationRegister", "name": "УОП_АктуальныеСпецификации"},
                                "sources": [{"role": "r2", "role_name": "Изменение рецептур", "chains": [{"type": "group_profile_role", "group": "g1", "profile": "p1"}]}],
                            }
                        ],
                        "data_restrictions": [],
                    },
                    {
                        "user": {"id": "u2", "name": "Anna.SIDOROVA", "active": True},
                        "groups": [],
                        "roles": [{"id": "r1", "name": "Чтение рецептур"}],
                        "permissions": [
                            {
                                "object": "000000C4:AAA",
                                "action": "Просмотр",
                                "object_resolution": {"guid": object_guid, "kind": "InformationRegister", "name": "УОП_АктуальныеСпецификации"},
                                "sources": [{"role": "r1", "role_name": "Чтение рецептур", "chains": [{"type": "direct_user_role", "user": "u2"}]}],
                            }
                        ],
                        "data_restrictions": [],
                    },
                ]
            },
        },
    )

    result = adapter_server.access_object_subjects(
        {"base_id": "upo_test", "ref": "РегистрСведений.УОП_АктуальныеСпецификации", "action": "write"}
    )

    assert result["status"] == "ok"
    assert result["object"]["guid"] == object_guid
    assert [role["id"] for role in result["roles"]] == ["r2"]
    assert result["profiles"] == [{"id": "p1", "name": "Технологи", "roles": ["r2"]}]
    assert result["groups"] == [{"id": "g1", "name": "Технолог", "profiles": ["p1"], "users": ["u1"]}]
    assert [item["user"]["id"] for item in result["users"]] == ["u1"]
    assert result["users"][0]["rights"] == {"read": False, "insert": False, "update": True, "delete": False}
    assert result["users"][0]["rights_detail"]["update"] == {"allowed": True, "unrestricted": True}


def test_access_rls_discover_returns_candidates_with_fields(monkeypatch: pytest.MonkeyPatch) -> None:
    listed_calls: list[tuple[str, str | None]] = []

    def fake_list_objects(
        kind: str | None = None,
        *,
        base_id: str,
        limit: int,
        offset: int,
        include_storage: bool = False,
        table: str = "Config",
        name_filter: str | None = None,
        **kwargs: Any,
    ) -> dict[str, Any]:
        listed_calls.append((str(kind), name_filter))
        if kind == "InformationRegister" and name_filter == "Доступ":
            return {
                "status": "ok",
                "objects": [
                    {
                        "guid": "11111111-1111-1111-1111-111111111111",
                        "kind": "InformationRegister",
                        "kind_ru": "РегистрСведений",
                        "name": "КлючиДоступаКОбъектам",
                        "storage": {"dbnames": [{"sql_number": 26003}]},
                    }
                ],
            }
        return {"status": "ok", "objects": []}

    monkeypatch.setattr(adapter_server, "list_objects", fake_list_objects)
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_attributes",
        lambda payload: {
            "status": "ok",
            "dimensions": [{"name": "Объект", "storage_routes": [{"sql_number": 26004}]}],
            "resources": [{"name": "КлючДоступа", "storage_routes": [{"sql_number": 26005}]}],
            "attributes": [],
            "tabular_sections": [],
        },
    )

    result = adapter_server.access_rls_discover({"base_id": "upo_test", "terms": ["Доступ"], "limit": 10})

    assert result["status"] == "ok"
    assert listed_calls
    assert result["counts"]["candidates"] == 1
    candidate = result["candidates"][0]
    assert candidate["name"] == "КлючиДоступаКОбъектам"
    assert "known_bsp_access_keys" in candidate["reasons"]
    assert candidate["fields"]["dimensions"][0]["name"] == "Объект"
    assert candidate["fields"]["resources"][0]["storage_routes"][0]["sql_number"] == 26005


def test_access_object_keys_resolve_enriches_rows(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "access_bsp_extractor_plan",
        lambda base_id: (
            {
                "access_object_keys": (
                    "SELECT N'0000006A:o1' AS object, N'0000006A' AS object_type_code, "
                    "106 AS object_sql_number, N'o1' AS object_id, N'k1' AS access_key"
                )
            },
            {"access_object_keys": {"object": "object", "object_type_code": "object_type_code", "object_sql_number": "object_sql_number", "object_id": "object_id", "access_key": "access_key"}},
            {"preset": "bsp"},
        ),
    )
    monkeypatch.setattr(
        adapter_server,
        "access_rows_from_query",
        lambda base_id, query, limit, timeout_seconds: ([{"object": "0000006A:o1", "object_type_code": "0000006A", "object_sql_number": 106, "object_id": "o1", "access_key": "k1"}], None, False),
    )
    monkeypatch.setattr(
        adapter_server,
        "access_resolve_object_key_records",
        lambda base_id, rows, timeout_seconds=60, max_records=200: {
            "rows": [{**rows[0], "object_presentation": "Заказ 1", "object_record": {"presentation": "Заказ 1"}}],
            "diagnostics": {"resolved": 1, "requested": 1},
        },
    )

    result = adapter_server.access_object_keys_resolve({"base_id": "upo_test", "limit": 10})

    row = result["areas"]["access_object_keys"]["rows"][0]
    assert row["object_presentation"] == "Заказ 1"
    assert result["areas"]["access_object_keys"]["record_resolution"] == {"resolved": 1, "requested": 1}


def test_access_object_explain_resolves_subjects(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "access_bsp_extractor_plan",
        lambda base_id: (
            {
                "access_object_keys": "object_sql",
                "access_group_keys": "group_key_sql",
                "access_user_keys": "user_key_sql",
                "access_set_keys": "set_key_sql",
                "users": "users_sql",
                "groups": "groups_sql",
                "group_users": "group_users_sql",
                "user_group_members": "user_group_members_sql",
            },
            {"access_object_keys": {"access_key": "access_key"}, "access_group_keys": {"group": "group"}, "access_user_keys": {"user": "user"}, "access_set_keys": {"access_set": "access_set"}},
            {"preset": "bsp"},
        ),
    )

    def fake_key_area(
        base_id: str,
        area: str,
        query: str,
        mapping: dict[str, str],
        payload: dict[str, Any],
        *,
        limit: int,
        offset: int,
        timeout_seconds: int,
    ) -> dict[str, Any]:
        rows_by_area = {
            "access_object_keys": [{"object": "0000006A:o1", "object_sql_number": 106, "object_id": "o1", "access_key": "k1"}],
            "access_group_keys": [{"group": "g1", "group_name": "Продажи", "access_key": "k1"}],
            "access_user_keys": [{"user": "u2", "user_set": "s2", "user_set_name": "Петров (Пользователь)", "access_key": "k1"}],
            "access_set_keys": [{"access_set": "s1", "access_set_name": "Набор", "access_key": "k1"}],
        }
        return {"area": area, "rows": rows_by_area[area], "counts": {"rows": len(rows_by_area[area]), "limit": limit, "offset": offset, "truncated": False}}

    monkeypatch.setattr(adapter_server, "access_key_query_run_area", fake_key_area)
    monkeypatch.setattr(
        adapter_server,
        "access_resolve_object_key_records",
        lambda base_id, rows, timeout_seconds=60, max_records=200: {
            "rows": [{**rows[0], "object_presentation": "Заказ 1"}],
            "diagnostics": {"resolved": 1},
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "access_rows_from_query",
        lambda base_id, query, limit, timeout_seconds: ([], None, False),
    )
    monkeypatch.setattr(
        adapter_server,
        "access_snapshot_from_extractor_rows",
        lambda rows_by_area, mappings: {
            "groups": [{"id": "g1", "name": "Продажи", "users": ["u1"]}],
            "users": [{"id": "u1", "name": "Иванов"}],
        },
    )
    monkeypatch.setattr(adapter_server, "access_resolve_user_names", lambda base_id, refs, diagnostics=None, timeout_seconds=60, max_refs=20000: {"U1": "Иванов из справочника"})

    result = adapter_server.access_object_explain({"base_id": "upo_test", "object_id": "o1"})

    assert result["status"] == "ok"
    assert result["object_keys"][0]["object_presentation"] == "Заказ 1"
    assert result["access_keys"] == ["K1"]
    assert result["groups"][0]["group_name"] == "Продажи"
    assert {user["name"] for user in result["users"]} == {"Иванов из справочника", "Петров (Пользователь)"}
    assert result["counts"]["users"] == 2


def test_access_role_profiles_returns_profiles_and_groups(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "access_bsp_extractor_plan",
        lambda base_id: (
            {
                "roles": "roles_sql",
                "profiles": "profiles_sql",
                "profile_roles": "profile_roles_sql",
                "group_profiles": "group_profiles_sql",
                "groups": "groups_sql",
            },
            {
                "roles": {"id": "id", "name": "name"},
                "profiles": {"id": "id", "name": "name"},
                "profile_roles": {"profile": "profile", "role": "role"},
                "group_profiles": {"group": "group", "profile": "profile"},
                "groups": {"id": "id", "name": "name"},
            },
            {"preset": "bsp"},
        ),
    )

    rows_by_query = {
        "roles_sql": [{"id": "r1", "name": "МенеджерПродаж"}],
        "profiles_sql": [{"id": "p1", "name": "Продажи"}, {"id": "p2", "name": "Склад"}],
        "profile_roles_sql": [{"profile": "p1", "role": "r1"}],
        "group_profiles_sql": [{"group": "g1", "profile": "p1"}, {"group": "g2", "profile": "p2"}],
        "groups_sql": [{"id": "g1", "name": "Отдел продаж"}, {"id": "g2", "name": "Склад"}],
    }
    monkeypatch.setattr(
        adapter_server,
        "access_rows_from_query",
        lambda base_id, query, limit, timeout_seconds: (rows_by_query[query], None, False),
    )

    result = adapter_server.access_role_profiles({"base_id": "upo_test", "role": "Менеджер"})

    assert result["status"] == "ok"
    assert result["roles"][0]["name"] == "МенеджерПродаж"
    assert result["profiles"] == [{"id": "p1", "name": "Продажи", "roles": ["r1"]}]
    assert result["groups"] == [{"id": "g1", "name": "Отдел продаж", "profiles": ["p1"]}]
    assert result["counts"] == {"roles": 1, "profiles": 1, "groups": 1}


def test_access_role_users_fuzzy_role_phrase_returns_users(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "access_bsp_extractor_plan",
        lambda base_id: (
            {
                "roles": "roles_sql",
                "profiles": "profiles_sql",
                "profile_roles": "profile_roles_sql",
                "group_profiles": "group_profiles_sql",
                "groups": "groups_sql",
                "group_users": "group_users_sql",
                "user_group_members": "user_group_members_sql",
                "users": "users_sql",
            },
            {
                "roles": {"id": "id", "name": "name"},
                "profiles": {"id": "id", "name": "name"},
                "profile_roles": {"profile": "profile", "role": "role"},
                "group_profiles": {"group": "group", "profile": "profile"},
                "groups": {"id": "id", "name": "name"},
                "group_users": {"group": "group", "user": "user"},
                "user_group_members": {"group": "group", "user": "user"},
                "users": {"id": "id", "name": "name", "active": "active"},
            },
            {"preset": "bsp"},
        ),
    )
    rows_by_query = {
        "roles_sql": [
            {"id": "r1", "name": "УПО. Добавление и изменение номенклатуры поставщиков (Роль)"},
            {"id": "r2", "name": "УПО. Чтение номенклатуры поставщиков (Роль)"},
        ],
        "profiles_sql": [{"id": "p1", "name": "УПО. Закупки"}],
        "profile_roles_sql": [{"profile": "p1", "role": "r1"}],
        "group_profiles_sql": [{"group": "g1", "profile": "p1"}],
        "groups_sql": [{"id": "g1", "name": "Закупщик"}],
        "group_users_sql": [{"group": "g1", "user": "0000015C:11111111111111111111111111111111"}],
        "user_group_members_sql": [],
        "users_sql": [{"id": "0000015C:11111111111111111111111111111111", "name": "Ivan.PETROV", "active": True}],
    }
    monkeypatch.setattr(
        adapter_server,
        "access_rows_from_query",
        lambda base_id, query, limit, timeout_seconds: (rows_by_query[query], None, False),
    )
    monkeypatch.setattr(adapter_server, "access_resolve_user_names", lambda *args, **kwargs: {"U1": "Ivan.PETROV"})

    result = adapter_server.access_role_users({"base_id": "upo_test", "role": "запись изменение номенклатура поставщиков"})

    assert result["status"] == "ok"
    assert result["roles"][0]["name"] == "УПО. Добавление и изменение номенклатуры поставщиков (Роль)"
    assert result["profiles"][0]["name"] == "УПО. Закупки"
    assert result["groups"][0]["name"] == "Закупщик"
    assert result["users"][0] == {
        "id": "0000015C:11111111111111111111111111111111",
        "name": "Ivan.PETROV",
        "active": True,
        "groups": [{"id": "g1", "name": "Закупщик"}],
        "role_sources": [{"type": "group_profile_role", "group": "g1", "profile": "p1", "role": "r1", "role_name": "УПО. Добавление и изменение номенклатуры поставщиков (Роль)"}],
    }
    assert "номенклатур" in result["diagnostics"]["role_match"]["tokens"]
    assert "поставщик" in result["diagnostics"]["role_match"]["tokens"]


def test_access_role_users_includes_direct_user_and_group_roles(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "access_bsp_extractor_plan",
        lambda base_id: (
            {
                "roles": "roles_sql",
                "profiles": "profiles_sql",
                "profile_roles": "profile_roles_sql",
                "group_profiles": "group_profiles_sql",
                "groups": "groups_sql",
                "group_users": "group_users_sql",
                "user_group_members": "user_group_members_sql",
                "users": "users_sql",
            },
            {
                "roles": {"id": "id", "name": "name"},
                "profiles": {"id": "id", "name": "name"},
                "profile_roles": {"profile": "profile", "role": "role"},
                "group_profiles": {"group": "group", "profile": "profile"},
                "groups": {"id": "id", "name": "name", "roles": "roles"},
                "group_users": {"group": "group", "user": "user"},
                "user_group_members": {"group": "group", "user": "user"},
                "users": {"id": "id", "name": "name", "active": "active", "roles": "roles"},
            },
            {"preset": "bsp"},
        ),
    )
    rows_by_query = {
        "roles_sql": [{"id": "r1", "name": "Прямая роль"}],
        "profiles_sql": [],
        "profile_roles_sql": [],
        "group_profiles_sql": [],
        "groups_sql": [{"id": "g1", "name": "Прямая группа", "roles": ["r1"]}],
        "group_users_sql": [{"group": "g1", "user": "u2"}],
        "user_group_members_sql": [],
        "users_sql": [
            {"id": "u1", "name": "Direct.USER", "active": True, "roles": ["r1"]},
            {"id": "u2", "name": "Group.USER", "active": True, "roles": []},
        ],
    }
    monkeypatch.setattr(
        adapter_server,
        "access_rows_from_query",
        lambda base_id, query, limit, timeout_seconds: (rows_by_query[query], None, False),
    )
    monkeypatch.setattr(adapter_server, "access_resolve_user_names", lambda *args, **kwargs: {})

    result = adapter_server.access_role_users({"base_id": "upo_test", "role": "Прямая роль"})

    assert result["status"] == "ok"
    users = {user["id"]: user for user in result["users"]}
    assert users["u1"]["role_sources"] == [{"type": "direct_user_role", "user": "u1", "role": "r1", "role_name": "Прямая роль"}]
    assert users["u2"]["role_sources"] == [{"type": "group_role", "group": "g1", "role": "r1", "role_name": "Прямая роль"}]
    assert users["u2"]["groups"] == [{"id": "g1", "name": "Прямая группа"}]


def test_access_role_audit_export_returns_flat_rows_and_csv(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "access_role_chain",
        lambda base_id, role_selector, timeout_seconds=120: {
            "status": "ok",
            "base_id": base_id,
            "query": {"role": role_selector},
            "roles": [{"id": "r1", "name": "УПО. Добавление и изменение номенклатуры поставщиков (Роль)"}],
            "profiles": [{"id": "p1", "name": "УПО. Закупки", "roles": ["r1"]}],
            "groups": [{"id": "g1", "name": "Закупщик", "profiles": ["p1"]}],
            "users": [{"id": "0000015C:11111111111111111111111111111111", "name": "Ivan.PETROV", "active": True, "groups": [{"id": "g1", "name": "Закупщик"}]}],
            "alternatives": [],
            "counts": {"roles": 1, "profiles": 1, "groups": 1, "users": 1},
            "diagnostics": {"role_match": {"best_score": 100}, "bsp": {"metadata": {"sql_numbers": {"users": 348, "external_users": 141, "user_groups": 149}}}},
        },
    )

    result = adapter_server.access_role_audit_export({"base_id": "upo_test", "role": "запись изменение номенклатура поставщиков", "format": "csv"})

    assert result["status"] == "ok"
    assert result["format"] == "csv"
    assert result["rows"][0]["matched_role_name"] == "УПО. Добавление и изменение номенклатуры поставщиков (Роль)"
    assert result["rows"][0]["profile_name"] == "УПО. Закупки"
    assert result["rows"][0]["group_name"] == "Закупщик"
    assert result["rows"][0]["user_name"] == "Ivan.PETROV"
    assert result["rows"][0]["user_type"] == "user"
    assert result["rows"][0]["user_active"] is True
    assert result["rows"][0]["user_marked"] is False
    assert result["rows"][0]["user_groups_count"] == 1
    assert result["rows"][0]["access_path"] == "УПО. Добавление и изменение номенклатуры поставщиков (Роль) -> УПО. Закупки -> Закупщик -> Ivan.PETROV"
    assert "matched_role_name,profile_id" in result["csv"]
    assert "access_path" in result["csv"]
    assert "Ivan.PETROV" in result["csv"]


def test_access_role_audit_export_includes_direct_role_sources(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "access_role_chain",
        lambda base_id, role_selector, timeout_seconds=120: {
            "status": "ok",
            "base_id": base_id,
            "query": {"role": role_selector},
            "roles": [{"id": "r1", "name": "Прямая роль"}],
            "profiles": [],
            "groups": [{"id": "g1", "name": "Прямая группа"}],
            "users": [
                {
                    "id": "u1",
                    "name": "Direct.USER",
                    "active": True,
                    "marked": True,
                    "user_type": "external_user",
                    "groups": [],
                    "role_sources": [{"type": "direct_user_role", "user": "u1", "role": "r1", "role_name": "Прямая роль"}],
                },
                {
                    "id": "u2",
                    "name": "Group.USER",
                    "active": True,
                    "groups": [{"id": "g1", "name": "Прямая группа"}],
                    "role_sources": [{"type": "group_role", "group": "g1", "role": "r1", "role_name": "Прямая роль"}],
                },
            ],
            "alternatives": [],
            "counts": {"roles": 1, "profiles": 0, "groups": 1, "users": 2},
            "diagnostics": {},
        },
    )

    result = adapter_server.access_role_audit_export({"base_id": "upo_test", "role": "Прямая роль"})

    assert result["status"] == "ok"
    assert [row["user_name"] for row in result["rows"]] == ["Direct.USER", "Group.USER"]
    assert result["rows"][0]["profile_name"] == ""
    assert result["rows"][0]["user_type"] == "external_user"
    assert result["rows"][0]["user_marked"] is True
    assert result["rows"][0]["access_path"] == "Прямая роль -> Direct.USER"
    assert result["rows"][1]["group_name"] == "Прямая группа"
    assert result["rows"][1]["access_path"] == "Прямая роль -> Прямая группа -> Group.USER"


def test_access_role_audit_analyze_returns_findings(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "access_role_audit_export",
        lambda payload: {
            "schema": "onec_access_role_audit_export.v1",
            "status": "ok",
            "base_id": payload["base_id"],
            "source": {"kind": "live_sql", "preset": "bsp"},
            "query": {"role": payload["role"]},
            "rows": [
                {
                    "matched_role_name": "Администратор системы (Роль)",
                    "profile_name": "Администратор",
                    "group_name": "Администраторы",
                    "user_id": "u1",
                    "user_name": "Ivan.PETROV",
                    "user_type": "external_user",
                    "user_active": True,
                    "user_marked": False,
                    "access_path": "Администратор системы (Роль) -> Администратор -> Администраторы -> Ivan.PETROV",
                }
            ],
            "roles": [{"id": "r1", "name": "Администратор системы (Роль)"}],
            "profiles": [{"id": "p1", "name": "Администратор"}],
            "groups": [{"id": "g1", "name": "Администраторы"}],
            "users": [{"id": "u1", "name": "Ivan.PETROV"}],
            "alternatives": [{"id": "r1", "name": "Администратор системы (Роль)"}, {"id": "r2", "name": "Администратор"}],
            "counts": {"roles": 1, "profiles": 1, "groups": 1, "users": 1, "rows": 1},
            "diagnostics": {},
        },
    )

    result = adapter_server.access_role_audit_analyze({"base_id": "upo_test", "role": "Администратор", "user_threshold": 1})

    assert result["status"] == "ok"
    assert result["risk_level"] == "high"
    codes = {finding["code"] for finding in result["findings"]}
    assert {"many_users", "fuzzy_role_alternatives", "broad_access_group", "external_users"}.issubset(codes)


def test_access_graph_adds_user_access_key_counts() -> None:
    graph = adapter_server.build_access_graph_from_snapshot(
        {
            "users": [{"id": "0000015C:u1", "name": "User"}],
            "groups": [{"id": "g1", "name": "Продажи", "users": ["0000015C:u1"]}],
            "access_keys": {
                "access_group_keys": [
                    {"group": "g1", "group_name": "Продажи", "access_key": "kg1"},
                    {"group_ref": "00000092:g2", "group_name": "Склад", "access_key": "kg2"},
                ],
                "access_user_keys": [
                    {"user": "0000015C:u1", "user_set": "s1", "access_key": "ku1"},
                    {"user": "0000015C:u2", "user_set": "s2", "access_key": "ku2"},
                ],
                "access_object_keys": [
                    {"object": "Document.Заказ", "object_type_code": "0000006A", "object_sql_number": 106, "object_id": "o1", "access_key": "kg1"},
                    {"object": "Document.Чужой", "access_key": "other"},
                ],
            },
        },
        base_id="upo_test",
    )

    access_keys = graph["effective_users"][0]["access_keys"]
    assert access_keys["counts"] == {"group_keys": 1, "user_keys": 1, "object_keys": 1, "object_key_types": 1, "total": 2}
    assert access_keys["samples"]["group_keys"][0]["access_key"] == "kg1"
    assert access_keys["samples"]["user_keys"][0]["access_key"] == "ku1"
    assert access_keys["samples"]["object_keys"][0]["object"] == "Document.Заказ"
    assert access_keys["samples"]["object_keys"][0]["object_sql_number"] == 106


def test_access_graph_uses_access_user_key_name_for_placeholder_user() -> None:
    graph = adapter_server.build_access_graph_from_snapshot(
        {
            "users": [{"id": "0000015C:u1", "name": "0000015C:u1"}],
            "access_keys": {
                "access_user_keys": [
                    {"user": "0000015C:u1", "user_set_name": "Иванов (Пользователь)", "user_set": "s1", "access_key": "ku1"},
                ],
            },
        },
        base_id="upo_test",
    )

    assert graph["effective_users"][0]["user"]["name"] == "Иванов (Пользователь)"


def test_access_user_explain_has_summary_and_can_resolve_object_key_samples(monkeypatch: pytest.MonkeyPatch) -> None:
    access = {
        "users": [{"id": "u1", "name": "Иванов", "groups": ["g1"]}],
        "groups": [{"id": "g1", "profiles": ["p1"]}],
        "profiles": [{"id": "p1", "roles": ["r1"]}],
        "roles": [{"id": "r1", "permissions": [{"object": "Catalog.Контрагенты", "object_name": "Контрагенты", "actions": ["Просмотр"]}]}],
        "access_keys": {
            "access_group_keys": [{"group": "g1", "access_key": "k1"}],
            "access_object_keys": [{"object": "0000006A:o1", "object_type_code": "0000006A", "object_sql_number": 106, "object_id": "o1", "access_key": "k1"}],
        },
    }
    monkeypatch.setattr(
        adapter_server,
        "access_resolve_object_key_records",
        lambda base_id, rows, timeout_seconds=60, max_records=200: {
            "rows": [{**rows[0], "object_presentation": "Объект 1"}],
            "diagnostics": {"resolved": 1},
        },
    )

    result = call_method("access.user.explain", {"base_id": "upo_test", "access": access, "user": "u1", "resolve_records": True})

    assert result["summary"]["permission_objects"] == ["Контрагенты"]
    assert "Иванов" in result["summary"]["text"]
    assert result["access_keys"]["samples"]["object_keys"][0]["object_presentation"] == "Объект 1"


def test_related_child_filter_accepts_guid_selector() -> None:
    guid = "e5afdaab-b6e1-452e-9b22-d40207bc7413"
    items = [
        {
            "category": "Template",
            "status": "ok",
            "guid": guid,
            "identity": {"name": "РасширеннаяИнформацияПоСДИЗ", "synonyms": {"ru": "Расширенная информация по СДИЗ"}},
        },
        {
            "category": "Template",
            "status": "ok",
            "guid": "11111111-1111-1111-1111-111111111111",
            "identity": {"name": "ДругойМакет"},
        },
    ]

    matches = filter_related_children_by_identity(items, "Template", guid)

    assert len(matches) == 1
    assert matches[0][0]["guid"] == guid
    assert matches[0][1] == "guid_exact"


def test_public_template_summary_exposes_known_platform_types() -> None:
    summary = adapter_server.public_template_summary(
        [
            {
                "role": "content",
                "features": {"tabular_document": True},
            }
        ]
    )

    assert summary["platform_type"] == "Табличный документ"
    assert summary["format"] == "ТабличныйДокумент"
    assert summary["template_type_candidates"][0]["id"] == "tabular_document"
    assert summary["template_type_candidates"][0]["confidence"] == "high"
    assert "Текстовый документ" in {item["name"] for item in summary["known_platform_types"]}
    assert "Внешняя компонента" in {item["name"] for item in summary["known_platform_types"]}
    assert next(item for item in summary["known_platform_types"] if item["id"] == "data_composition_schema")["status"] == "read_and_scalar_patch_supported"
    assert {item["name"] for item in summary["known_platform_types"]} == {
        "Табличный документ",
        "Текстовый документ",
        "Двоичные данные",
        "Active document",
        "HTML документ",
        "Географическая схема",
        "Графическая схема",
        "Схема компоновки данных",
        "Макет оформления компоновки данных",
        "Внешняя компонента",
    }


def test_public_template_summary_uses_declared_template_type_alias() -> None:
    summary = adapter_server.public_template_summary(
        [
            {
                "role": "metadata_payload",
                "template_type": "DataCompositionAppearanceTemplate",
            },
            {
                "role": "content",
                "features": {},
            },
        ]
    )

    assert summary["features"]["declared_platform_type_id"] == "data_composition_appearance_template"
    assert summary["features"]["declared_platform_xml_type"] == "DataCompositionAppearanceTemplate"
    assert summary["platform_type"] == "Макет оформления компоновки данных"
    assert summary["template_type_candidates"][0]["id"] == "data_composition_appearance_template"
    assert summary["template_type_candidates"][0]["confidence"] == "high"


def test_public_template_summary_recognizes_live_appearance_xml_root() -> None:
    summary = adapter_server.public_template_summary(
        [{"role": "content", "classification": {"xml_root": "AppearanceTemplate"}, "features": {}}]
    )

    assert summary["platform_type"] == "Двоичные данные"
    assert summary["template_type_candidates"][0]["id"] == "binary_data"
    assert summary["content_type_evidence"] == {
        "xml_roots": ["AppearanceTemplate"],
        "meaning": "payload_carrier_only_not_platform_template_type",
    }


def test_public_template_summary_exposes_unmapped_descriptor_type_code() -> None:
    summary = adapter_server.public_template_summary(
        [
            {
                "role": "metadata_payload",
                "classification": {
                    "tree": {
                        "type": "list",
                        "items": [
                            {"type": "atom", "value": "1"},
                            {"type": "list", "items": [
                                {"type": "atom", "value": "4"},
                                {"type": "list", "items": []},
                                {"type": "atom", "value": "7"},
                            ]},
                        ],
                    },
                },
            },
            {"role": "content", "classification": {"xml_root": "AppearanceTemplate"}, "features": {}},
        ]
    )

    assert summary["platform_type"] == "Двоичные данные"
    assert summary["descriptor_type_evidence"]["codes"] == ["7"]
    assert summary["descriptor_type_evidence"]["status"] == "unresolved_mapping"


def test_common_template_inventory_uses_parts_classifier(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "resolve_object_guid", lambda *_args, **_kwargs: ("a" * 36, None, {"name": "Оформление", "ref": "CommonTemplate.Оформление"}, None))
    monkeypatch.setattr(adapter_server, "read_template_by_guid", lambda _payload: {"status": "ok", "source": {"kind": "live_sql"}, "templates": [{"guid": "a" * 36}]})
    monkeypatch.setattr(adapter_server, "metadata_object_parts", lambda _payload: {"parts": [{"role": "content", "classification": {"xml_root": "AppearanceTemplate"}, "features": {}}]})

    result = adapter_server.metadata_object_templates({"base_id": "upo_test", "kind": "CommonTemplate", "name": "Оформление"})

    assert result["templates"][0]["platform_type"] == "Двоичные данные"
    assert result["templates"][0]["content_type_evidence"]["xml_roots"] == ["AppearanceTemplate"]


def test_common_template_inventory_preserves_unmapped_descriptor_code(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "resolve_object_guid", lambda *_args, **_kwargs: ("a" * 36, None, {"name": "Оформление", "ref": "CommonTemplate.Оформление"}, None))
    monkeypatch.setattr(adapter_server, "read_template_by_guid", lambda _payload: {"status": "ok", "source": {"kind": "live_sql"}, "templates": [{"guid": "a" * 36}]})
    monkeypatch.setattr(adapter_server, "metadata_object_parts", lambda _payload: {"parts": [{
        "classification": {"role": "metadata_payload", "tree": {"type": "list", "items": [
            {"type": "atom", "value": "1"},
            {"type": "list", "items": [
                {"type": "atom", "value": "4"}, {"type": "list", "items": []}, {"type": "atom", "value": "7"},
            ]},
        ]}},
    }, {"classification": {"role": "binary_or_unknown_payload", "xml_root": "AppearanceTemplate"}, "features": {}}]})

    result = adapter_server.metadata_object_templates({"base_id": "upo_test", "kind": "CommonTemplate", "name": "Оформление"})

    assert result["templates"][0]["descriptor_type_evidence"]["codes"] == ["7"]
    assert result["templates"][0]["name"] == "Оформление"


def test_top_level_common_template_summary_preserves_descriptor_code(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "resolve_object_guid", lambda *_args, **_kwargs: ("a" * 36, None, {"name": "Оформление", "ref": "CommonTemplate.Оформление"}, None))
    monkeypatch.setattr(adapter_server, "read_template_by_guid", lambda _payload: {"status": "ok", "source": {"kind": "live_sql"}, "templates": [{"guid": "a" * 36}]})
    monkeypatch.setattr(adapter_server, "metadata_object_parts", lambda _payload: {"parts": [{
        "classification": {"role": "metadata_payload", "tree": {"type": "list", "items": [
            {"type": "atom", "value": "1"}, {"type": "list", "items": [
                {"type": "atom", "value": "4"}, {"type": "list", "items": []}, {"type": "atom", "value": "7"},
            ]},
        ]}},
    }, {"classification": {"role": "binary_or_unknown_payload", "xml_root": "AppearanceTemplate"}}]})

    result = adapter_server.metadata_object_templates({"base_id": "upo_test", "kind": "CommonTemplate", "name": "Оформление"})

    assert result["templates"][0]["descriptor_type_evidence"]["codes"] == ["7"]


def test_moxel_template_artifact_kind_tracks_coordinate_hint_schema() -> None:
    assert adapter_server.MOXEL_TEMPLATE_ARTIFACT_KIND == "template_part_moxel_v11"


def test_template_structure_semantic_text_includes_coordinate_hints() -> None:
    text = adapter_server.template_structure_semantic_text(
        {
            "format": "MOXCEL",
            "capacity_dimensions": {"rows": 75, "columns": 26},
            "used_dimensions": {"rows": 75, "columns": 26},
            "cell_coordinate_hints": [
                {
                    "text": "ПредставлениеПодотчетногоЛица",
                    "one_based": {"row": 22, "column": 7},
                }
            ],
        }
    )

    assert "cell_hint R22C7: ПредставлениеПодотчетногоЛица" in text


def test_limit_object_commands_result_applies_total_limit() -> None:
    object_commands = [{"name": "ОбъектнаяКоманда"}]
    form_commands = [{"name": "ФормаКоманда1"}, {"name": "ФормаКоманда2"}]

    limited_object, limited_form, commands, counts = limit_object_commands_result(object_commands, form_commands, 2)

    assert commands == [{"name": "ОбъектнаяКоманда"}, {"name": "ФормаКоманда1"}]
    assert limited_object == [{"name": "ОбъектнаяКоманда"}]
    assert limited_form == [{"name": "ФормаКоманда1"}]
    assert counts == {"commands_total": 3, "commands_truncated": True, "max_commands": 2}


def test_definition_find_validation_rejects_bad_query_and_areas() -> None:
    result = adapter_server.validate_metadata_definition_find_payload(
        {"base_id": "upo_test", "kind": "Document", "name": "АвансовыйОтчет", "query": ["Реквизит"]}
    )
    assert result is not None
    assert result["status"] == "invalid_argument"
    assert result["argument"] == "query"

    result = adapter_server.validate_metadata_definition_find_payload(
        {"base_id": "upo_test", "kind": "Document", "name": "АвансовыйОтчет", "query": "Реквизит", "areas": ["unknown"]}
    )
    assert result is not None
    assert result["status"] == "invalid_argument"
    assert result["argument"] == "areas"

    result = adapter_server.validate_metadata_definition_find_payload(
        {"base_id": "upo_test", "query": "Реквизит", "areas": ["metadata"], "use_cache": "true"}
    )
    assert result is not None
    assert result["status"] == "invalid_argument"
    assert result["argument"] == "use_cache"


def test_definition_match_public_location_and_origin() -> None:
    object_card = {"kind": "Document", "kind_ru": "Документ", "name": "Заказ", "guid": "object-guid", "source": "base"}
    match = adapter_server.definition_match(
        query="Количество",
        area="object",
        kind_ru="Реквизит табличной части",
        location={"presentation": "Документ.Заказ.ТабличныеЧасти.Товары.Количество"},
        item={"name": "Количество", "type": {"presentation": "Число(15,3)"}},
        object_card=object_card,
        read_selector={"method": "metadata.object.attributes"},
    )

    assert match is not None
    assert match["match_by"] == "name_exact"
    assert match["origin"] == {"source": "configuration", "presentation": "Конфигурация", "extension": None, "status": "ok"}
    assert match["type"]["presentation"] == "Число(15,3)"
    assert match["location"]["presentation"] == "Документ.Заказ.ТабличныеЧасти.Товары.Количество"


def test_definition_match_finds_form_module_routine(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "get_object",
        lambda *args, **kwargs: {
            "status": "ok",
            "source": {"kind": "live_metadata"},
            "object": {
                "guid": "cfcf9913-41db-4fb7-974f-add97b7144ec",
                "kind": "Document",
                "kind_ru": "Документ",
                "name": "УОП_ПланированиеМеню",
                "synonym": "Планирование производства (Документ)",
                "source": "base",
            },
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_form_details",
        lambda payload: {
            "status": "ok",
            "forms": [
                {
                    "guid": "ccc6b506-1dce-4a4c-b3c7-3b5cd4252cb5",
                    "name": "ФормаДокумента",
                    "synonyms": {"ru": "Форма документа"},
                    "attributes": [],
                    "elements": [],
                    "commands": [],
                    "events": [],
                    "module": {
                        "routine_count": 1,
                        "routines_sample": [
                            {"kind": "Процедура", "name": "ЗаполнитьПроизводствоНаСервере"},
                        ],
                    },
                }
            ],
        },
    )

    result = adapter_server.metadata_definition_find(
        {
            "base_id": "upo_test",
            "kind": "Document",
            "name": "УОП_ПланированиеМеню",
            "form": "ФормаДокумента",
            "query": "ЗаполнитьПроизводствоНаСервере",
            "areas": ["form"],
        }
    )

    assert result["status"] == "ok"
    assert result["matches"][0]["kind"] == "Процедура формы"
    assert result["matches"][0]["location"]["section"] == "Модуль формы"
    assert result["matches"][0]["form"]["name"] == "ФормаДокумента"


def test_metadata_object_form_details_supports_common_form(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
        seen["payload"] = payload
        return {
            "status": "ok",
            "source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": "form-guid.0"},
            "form": {"kind": "CommonForm", "name": "t_Форма", "guid": "form-guid"},
            "profile": {
                "items": [{"name": "Кнопка1", "section": "items"}],
                "attributes": [],
                "commands": [{"name": "ЗаменаДомена", "action": "ЗаменаДомена", "section": "commands"}],
                "events": [],
                "module": {"routine_count": 1, "routines_sample": [{"kind": "Процедура", "name": "ЗаменаДомена"}]},
            },
        }

    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)

    result = adapter_server.metadata_object_form_details(
        {
            "base_id": "upo_test",
            "extension": "test2",
            "object_type": "CommonForm",
            "object_name": "t_Форма",
            "table": "ConfigCASSave",
        }
    )

    assert result["status"] == "ok"
    assert result["object"] == {"kind": "CommonForm", "name": "t_Форма", "guid": "form-guid"}
    assert result["forms"][0]["commands"][0]["name"] == "ЗаменаДомена"
    assert result["forms"][0]["module"]["routines_sample"][0]["name"] == "ЗаменаДомена"
    assert seen["payload"]["object_type"] == "CommonForm"
    assert seen["payload"]["object_name"] == "t_Форма"
    assert seen["payload"]["form"] == "t_Форма"
    assert seen["payload"]["include_storage"] is False


def test_form_field_lineage_exact_mode_filters_related_price_variants(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, object] = {}

    def fake_form_details(payload: dict[str, object]) -> dict[str, object]:
        seen["payload"] = payload
        return {
            "status": "ok",
            "forms": [{
                "name": "ФормаДокумента",
                "elements": [
                    {"name": "ВидыКоличествЦенаПродажи", "type_name": "Поле ввода"},
                    {"name": "ВидыКоличествЦенаПродажиСНДС", "type_name": "Поле ввода"},
                    {"name": "ВидыКоличествЦенаПродажиРасширеннаяПодсказка", "type_name": "Расширенная подсказка"},
                ],
                "attributes": [], "commands": [], "dynamic_lists": [],
            }],
        }

    monkeypatch.setattr(
        adapter_server,
        "metadata_object_form_details",
        fake_form_details,
    )

    result = adapter_server.form_field_lineage({
        "base_id": "upo_test", "ref": "Document.ПланированиеМеню",
        "field": "ВидыКоличествЦенаПродажи", "match_mode": "exact",
        "include_call_chain": False, "include_calculation": False, "include_binding_inference": False,
    })

    assert result["status"] == "ok"
    assert [item["item"]["name"] for item in result["matches"]] == ["ВидыКоличествЦенаПродажи"]
    assert result["matches"][0]["match_by"] == "exact"
    assert "calculation" not in result
    assert seen["payload"]["include_parameters"] is False


def test_form_field_lineage_rejects_non_boolean_and_unbounded_trace_arguments() -> None:
    invalid_bool = adapter_server.form_field_lineage({
        "base_id": "upo_test", "ref": "Document.ПланированиеМеню", "field": "Цена",
        "include_call_chain": "false",
    })
    invalid_depth = adapter_server.form_field_lineage({
        "base_id": "upo_test", "ref": "Document.ПланированиеМеню", "field": "Цена",
        "call_chain_depth": 4,
    })

    assert invalid_bool["status"] == "invalid_argument"
    assert invalid_bool["argument"] == "include_call_chain"
    assert invalid_depth["status"] == "invalid_argument"
    assert invalid_depth["argument"] == "call_chain_depth"


def test_form_field_lineage_does_not_infer_sales_price_for_generic_price(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server, "metadata_object_form_details",
        lambda payload: {"status": "ok", "forms": [{
            "name": "ФормаДокумента",
            "elements": [
                {"name": "ЦенаЗакупки", "type_name": "Поле ввода"},
                {"name": "ЦенаПродажи", "type_name": "Поле ввода"},
            ],
            "attributes": [], "commands": [], "dynamic_lists": [],
        }]},
    )
    monkeypatch.setattr(adapter_server, "code_read", lambda payload: pytest.fail("price calculation must not be inferred"))

    result = adapter_server.form_field_lineage({
        "base_id": "upo_test", "kind": "Document", "name": "ПланированиеМеню", "field": "Цена",
        "include_call_chain": False, "include_binding_inference": False,
    })

    assert result["status"] == "ok"
    assert "calculation" not in result
    assert result["match_summary"]["count"] == 2
    assert result["match_summary"]["ambiguous"] is True
    assert result["match_summary"]["recommended_match_mode"] == "exact"
    assert [item["field"] for item in result["match_summary"]["candidates"]] == ["ЦенаЗакупки", "ЦенаПродажи"]
    assert [item["params"]["field"] for item in result["match_summary"]["next_calls"]] == ["ЦенаЗакупки", "ЦенаПродажи"]
    assert all(item["params"]["match_mode"] == "exact" for item in result["match_summary"]["next_calls"])
    assert result["match_summary"]["next_calls"][0]["params"]["kind"] == "Document"
    assert result["match_summary"]["next_calls"][0]["params"]["name"] == "ПланированиеМеню"


def test_form_field_lineage_filters_by_decoded_form_title(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server, "metadata_object_form_details",
        lambda payload: {"status": "ok", "forms": [
            {"name": "FormA", "title": "Планирование производства", "elements": [{"name": "Цена", "type_name": "Поле ввода"}], "attributes": [], "commands": [], "dynamic_lists": []},
            {"name": "FormB", "title": "Продажи", "elements": [{"name": "Цена", "type_name": "Поле ввода"}], "attributes": [], "commands": [], "dynamic_lists": []},
        ]},
    )

    result = adapter_server.form_field_lineage({
        "base_id": "upo_test", "ref": "Document.ПланированиеМеню", "form_title": "Планирование производства",
        "field": "Цена", "match_mode": "exact", "include_call_chain": False, "include_calculation": False, "include_binding_inference": False,
    })

    assert result["status"] == "ok"
    assert [item["form"] for item in result["matches"]] == ["FormA"]
    assert result["matches"][0]["form_title_match"] == "confirmed"
    assert result["form_title_filter"]["status"] == "confirmed"


def test_form_field_lineage_reports_unverifiable_form_title_filter(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "metadata_object_form_details", lambda payload: {"status": "ok", "forms": [{
        "name": "ФормаДокумента", "elements": [{"name": "ДатаПлана", "type_name": "Поле ввода"}],
        "attributes": [], "commands": [], "dynamic_lists": [],
    }]})

    result = adapter_server.form_field_lineage({
        "base_id": "upo_test", "ref": "Document.ПланированиеМеню", "form_title": "Планирование производства",
        "field": "ДатаПлана", "match_mode": "exact", "include_call_chain": False,
        "include_calculation": False, "include_binding_inference": False,
    })

    assert result["status"] == "ok"
    assert result["matches"][0]["form_title_match"] == "unavailable"
    assert result["form_title_filter"]["status"] == "partial"
    assert result["form_title_filter"]["unavailable_forms"] == ["ФормаДокумента"]


def test_form_field_lineage_title_diagnostic_lists_only_matching_forms(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "metadata_object_form_details", lambda payload: {"status": "ok", "forms": [
        {"name": "ФормаДокумента", "elements": [{"name": "ДатаПлана"}], "attributes": [], "commands": [], "dynamic_lists": []},
        {"name": "ФормаСписка", "elements": [{"name": "Номер"}], "attributes": [], "commands": [], "dynamic_lists": []},
    ]})

    result = adapter_server.form_field_lineage({
        "base_id": "upo_test", "ref": "Document.ПланированиеМеню", "form_title": "Планирование производства",
        "field": "ДатаПлана", "match_mode": "exact", "include_call_chain": False,
        "include_calculation": False, "include_binding_inference": False,
    })

    assert result["form_title_filter"]["unavailable_forms"] == ["ФормаДокумента"]


def test_form_field_lineage_exact_ambiguity_provides_form_scoped_next_calls(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "metadata_object_form_details", lambda payload: {"status": "ok", "forms": [
        {"name": "ФормаДокумента", "elements": [{"name": "ДатаПлана"}], "attributes": [{"name": "ДатаПлана"}], "commands": [], "dynamic_lists": []},
        {"name": "ФормаСписка", "elements": [{"name": "ДатаПлана"}], "attributes": [], "commands": [], "dynamic_lists": []},
    ]})

    result = adapter_server.form_field_lineage({
        "base_id": "upo_test", "ref": "Document.ПланированиеМеню", "field": "ДатаПлана",
        "match_mode": "exact", "include_call_chain": False,
        "include_calculation": False, "include_binding_inference": False,
    })

    assert result["match_summary"]["ambiguous"] is True
    assert [call["params"]["form"] for call in result["match_summary"]["next_calls"]] == ["ФормаДокумента", "ФормаСписка"]


def test_form_field_lineage_reads_dynamic_list_column_and_query(monkeypatch: pytest.MonkeyPatch) -> None:
    query = "ВЫБРАТЬ Номенклатура.Себестоимость КАК FoodCost ИЗ Справочник.Номенклатура КАК Номенклатура"
    monkeypatch.setattr(adapter_server, "metadata_object_form_details", lambda payload: {"status": "ok", "forms": [{
        "name": "ФормаСписка", "elements": [], "commands": [], "dynamic_lists": [],
        "attributes": [{
            "name": "Список", "dynamic_list_settings": {"query_text": query},
            "dynamic_list_fields": [{"name": "FoodCost", "data_name": "FoodCost", "path_to_data": "Список.FoodCost"}],
        }],
    }]})

    result = adapter_server.form_field_lineage({
        "base_id": "upo_test", "ref": "Document.ПланированиеМеню", "field": "FoodCost",
        "match_mode": "exact", "include_call_chain": False,
        "include_calculation": False, "include_binding_inference": False,
    })

    match = result["matches"][0]
    assert match["section"] == "dynamic_list_fields"
    assert match["evidence"] == "decoded_dynamic_list_field"
    assert match["item"]["dynamic_list"] == "Список"
    assert match["item"]["data_path"] == "Список.FoodCost"
    assert match["item"]["expression"] == "Номенклатура.Себестоимость"
    assert match["item"]["query_text"] == query
    assert match["item"]["query_sources"] == [{"object_ref": "Справочник.Номенклатура", "alias": "Номенклатура"}]
    assert match["item"]["source_references"] == [{
        "alias": "Номенклатура", "field": "Себестоимость",
        "source": "Справочник.Номенклатура", "public_path": "Справочник.Номенклатура.Себестоимость",
    }]
    assert match["item"]["query_lineage"] == {
        "status": "confirmed", "parser": "simple_select_alias_v1",
        "output_field": "FoodCost", "expression": "Номенклатура.Себестоимость", "binding_kind": "direct_field",
    }
    assert match["item"]["origin_graph"] == {
        "status": "confirmed",
        "nodes": [
            {"id": "form_field:Список.FoodCost", "kind": "form_dynamic_list_field", "name": "FoodCost", "data_path": "Список.FoodCost"},
            {"id": "query_output:Список.FoodCost", "kind": "query_output", "name": "FoodCost", "expression": "Номенклатура.Себестоимость"},
            {"id": "source:Справочник.Номенклатура.Себестоимость", "kind": "metadata_field", "public_path": "Справочник.Номенклатура.Себестоимость", "source": "Справочник.Номенклатура", "field": "Себестоимость"},
        ],
        "edges": [
            {"from": "form_field:Список.FoodCost", "to": "query_output:Список.FoodCost", "kind": "dynamic_list_column_binding"},
            {"from": "query_output:Список.FoodCost", "to": "source:Справочник.Номенклатура.Себестоимость", "kind": "query_source_reference"},
        ],
    }


def test_form_read_methods_accept_isolated_job_mode(monkeypatch: pytest.MonkeyPatch) -> None:
    started: list[dict[str, Any]] = []
    monkeypatch.setattr(adapter_server, "adapter_start_job", lambda request: started.append(request) or {"status": "queued", "job_id": "job-form"})

    details = adapter_server.metadata_object_form_details({
        "base_id": "upo_test", "ref": "Document.ПланированиеМеню", "execution_mode": "job", "timeout_seconds": 17,
    })
    decoded = adapter_server.metadata_form_decode({
        "base_id": "upo_test", "ref": "Document.ПланированиеМеню", "execution_mode": "job", "timeout_seconds": 19,
    })
    lineage = adapter_server.form_field_lineage({
        "base_id": "upo_test", "ref": "Document.ПланированиеМеню", "field": "Цена", "execution_mode": "job", "timeout_seconds": 21,
    })

    assert details == {"status": "queued", "job_id": "job-form"}
    assert decoded == {"status": "queued", "job_id": "job-form"}
    assert lineage == {"status": "queued", "job_id": "job-form"}
    assert [request["method"] for request in started] == ["metadata.object.form.details", "metadata.form.decode", "form.field.lineage"]
    assert [request["timeout_seconds"] for request in started] == [17, 19, 21]
    assert [request["payload"]["execution_mode"] for request in started if "execution_mode" in request["payload"]] == []
    assert [request["payload"]["ref"] for request in started] == ["Document.ПланированиеМеню", "Document.ПланированиеМеню", "Document.ПланированиеМеню"]


def test_form_item_coverage_separates_auxiliary_records() -> None:
    summary = form_item_coverage_summary([
        {"type_name": "Поле ввода", "semantic": {"coverage": {"mapped": 4, "unmapped": 2}}},
        {"type_name": "Контекстное меню", "semantic": {"coverage": {"mapped": 1, "unmapped": 20}}},
        {"type_name": "Расширенная подсказка", "semantic": {"coverage": {"mapped": 1, "unmapped": 10}}},
    ])

    assert summary == {
        "all_items": {"items": 3, "mapped": 6, "unmapped": 32, "total": 38, "status": "partial"},
        "interactive_items": {"items": 1, "mapped": 4, "unmapped": 2, "total": 6, "status": "partial"},
        "auxiliary_items": {"items": 2, "mapped": 2, "unmapped": 30, "total": 32, "status": "partial"},
    }


def test_form_write_mode_is_not_forwarded_to_internal_form_decode() -> None:
    assert adapter_server.without_form_decode_selector_keys({
        "base_id": "upo_test", "form": "Тест", "element": "Поле",
        "execution_mode": "apply_and_rollback", "mode": "apply_and_rollback",
    }) == {"base_id": "upo_test", "form": "Тест"}


def test_form_field_lineage_marks_derived_dynamic_list_query_as_partial(monkeypatch: pytest.MonkeyPatch) -> None:
    query = (
        "ВЫБРАТЬ Позиции.Себестоимость КАК FoodCost "
        "ИЗ (ВЫБРАТЬ Номенклатура.Себестоимость КАК Себестоимость "
        "ИЗ Справочник.Номенклатура КАК Номенклатура) КАК Позиции"
    )
    monkeypatch.setattr(adapter_server, "metadata_object_form_details", lambda payload: {"status": "ok", "forms": [{
        "name": "ФормаСписка", "elements": [], "commands": [], "dynamic_lists": [],
        "attributes": [{
            "name": "Список", "dynamic_list_settings": {"query_text": query},
            "dynamic_list_fields": [{"name": "FoodCost", "data_name": "FoodCost"}],
        }],
    }]})

    result = adapter_server.form_field_lineage({
        "base_id": "upo_test", "ref": "Document.ПланированиеМеню", "field": "FoodCost",
        "match_mode": "exact", "include_call_chain": False,
        "include_calculation": False, "include_binding_inference": False,
    })

    lineage = result["matches"][0]["item"]["query_lineage"]
    assert lineage["status"] == "partial"
    assert lineage["diagnostics"] == ["complex_query_not_fully_traced"]
    assert lineage["complex_constructs"] == ["nested_query"]
    assert result["matches"][0]["item"]["source_references"] == [{
        "alias": "Позиции", "field": "Себестоимость",
        "source": "Справочник.Номенклатура", "public_path": "Справочник.Номенклатура.Себестоимость",
        "derived_from": {"alias": "Позиции", "field": "Себестоимость"},
        "resolution": "derived_table_projection",
    }]


def test_form_field_lineage_connects_visual_element_to_dynamic_list_alias(monkeypatch: pytest.MonkeyPatch) -> None:
    query = "ВЫБРАТЬ Номенклатура.Себестоимость КАК FoodCost ИЗ Справочник.Номенклатура КАК Номенклатура"
    monkeypatch.setattr(adapter_server, "metadata_object_form_details", lambda payload: {"status": "ok", "forms": [{
        "name": "ФормаПланирования", "commands": [], "dynamic_lists": [],
        "elements": [{"name": "Себестоимость", "path_to_data": "Список.FoodCost"}],
        "attributes": [{
            "name": "Список", "dynamic_list_settings": {"query_text": query},
            "dynamic_list_fields": [{"name": "FoodCost", "data_name": "FoodCost"}],
        }],
    }]})

    result = adapter_server.form_field_lineage({
        "base_id": "upo_test", "ref": "Document.ПланированиеМеню", "field": "Себестоимость",
        "match_mode": "exact", "include_call_chain": False,
        "include_calculation": False, "include_binding_inference": False,
    })

    binding = result["matches"][0]["item"]["dynamic_list_binding"]
    assert binding["status"] == "confirmed"
    assert binding["data_path"] == "Список.FoodCost"
    assert binding["field"] == "FoodCost"
    assert binding["query_lineage"]["status"] == "confirmed"
    assert binding["origin_graph"]["nodes"][0] == {
        "id": "form_element:Себестоимость", "kind": "form_element",
        "name": "Себестоимость", "data_path": "Список.FoodCost",
    }
    assert binding["origin_graph"]["edges"][0] == {
        "from": "form_element:Себестоимость", "to": "form_field:FoodCost",
        "kind": "visual_element_dynamic_list_binding", "evidence": "decoded_form_data_path",
    }


def test_form_field_lineage_marks_dynamic_list_calculation_and_its_inputs(monkeypatch: pytest.MonkeyPatch) -> None:
    query = (
        "ВЫБРАТЬ ВЫБОР КОГДА Номенклатура.Цена > 0 ТОГДА Номенклатура.Цена "
        "ИНАЧЕ Номенклатура.Себестоимость КОНЕЦ КАК FoodCost "
        "ИЗ Справочник.Номенклатура КАК Номенклатура"
    )
    monkeypatch.setattr(adapter_server, "metadata_object_form_details", lambda payload: {"status": "ok", "forms": [{
        "name": "ФормаСписка", "elements": [], "commands": [], "dynamic_lists": [],
        "attributes": [{"name": "Список", "dynamic_list_settings": {"query_text": query},
                        "dynamic_list_fields": [{"name": "FoodCost"}]}],
    }]})

    result = adapter_server.form_field_lineage({
        "base_id": "upo_test", "ref": "Document.ПланированиеМеню", "field": "FoodCost",
        "match_mode": "exact", "include_call_chain": False,
        "include_calculation": False, "include_binding_inference": False,
    })

    item = result["matches"][0]["item"]
    assert item["query_lineage"]["status"] == "partial"
    assert item["query_lineage"]["binding_kind"] == "calculated_expression"
    assert item["query_lineage"]["diagnostics"] == ["calculated_expression"]
    assert item["source_references"] == [
        {"alias": "Номенклатура", "field": "Цена", "source": "Справочник.Номенклатура", "public_path": "Справочник.Номенклатура.Цена"},
        {"alias": "Номенклатура", "field": "Себестоимость", "source": "Справочник.Номенклатура", "public_path": "Справочник.Номенклатура.Себестоимость"},
    ]


def test_form_field_lineage_returns_dynamic_list_query_context(monkeypatch: pytest.MonkeyPatch) -> None:
    query = (
        "ВЫБРАТЬ Номенклатура.Цена КАК Цена ИЗ Справочник.Номенклатура КАК Номенклатура "
        "ГДЕ Номенклатура.Вид = &Вид И Номенклатура.ПометкаУдаления = ЛОЖЬ "
        "УПОРЯДОЧИТЬ ПО Номенклатура.Наименование"
    )
    monkeypatch.setattr(adapter_server, "metadata_object_form_details", lambda payload: {"status": "ok", "forms": [{
        "name": "ФормаСписка", "elements": [], "commands": [], "dynamic_lists": [],
        "attributes": [{"name": "Список", "dynamic_list_settings": {"query_text": query},
                        "dynamic_list_fields": [{"name": "Цена"}]}],
    }]})

    result = adapter_server.form_field_lineage({
        "base_id": "upo_test", "ref": "Document.ПланированиеМеню", "field": "Цена",
        "match_mode": "exact", "include_call_chain": False,
        "include_calculation": False, "include_binding_inference": False,
    })

    assert result["matches"][0]["item"]["query_context"] == {
        "parameters": ["Вид"],
        "filter": "Номенклатура.Вид = &Вид И Номенклатура.ПометкаУдаления = ЛОЖЬ",
        "order_by": "Номенклатура.Наименование",
        "status": "static_query_context",
    }


def test_form_field_lineage_infers_dynamic_list_parameter_from_form_attribute(monkeypatch: pytest.MonkeyPatch) -> None:
    query = "ВЫБРАТЬ Номенклатура.Цена КАК Цена ИЗ Справочник.Номенклатура КАК Номенклатура ГДЕ Номенклатура.Вид = &ВидЦен"
    monkeypatch.setattr(adapter_server, "metadata_object_form_details", lambda payload: {"status": "ok", "forms": [{
        "name": "ФормаСписка", "elements": [{"name": "ВидЦен", "path_to_data": "ВидЦен"}], "commands": [], "dynamic_lists": [],
        "attributes": [
            {"name": "Список", "dynamic_list_settings": {"query_text": query}, "dynamic_list_fields": [{"name": "Цена"}]},
            {"name": "ВидЦен", "path_to_data": "Настройки.ВидЦен"},
        ],
    }]})

    result = adapter_server.form_field_lineage({
        "base_id": "upo_test", "ref": "Document.ПланированиеМеню", "field": "Цена",
        "match_mode": "exact", "include_call_chain": False,
        "include_calculation": False, "include_binding_inference": False,
    })

    assert result["matches"][0]["item"]["query_context"]["parameter_bindings"] == [{
        "parameter": "ВидЦен", "status": "inferred", "source": "decoded_name_or_data_path",
        "candidates": [
            {"kind": "form_attribute", "name": "ВидЦен", "data_path": "Настройки.ВидЦен"},
            {"kind": "form_element", "name": "ВидЦен", "data_path": "ВидЦен"},
        ],
    }]


def test_form_field_lineage_describes_join_and_virtual_register_source(monkeypatch: pytest.MonkeyPatch) -> None:
    query = (
        "ВЫБРАТЬ Цены.Цена КАК Цена ИЗ Справочник.Номенклатура КАК Номенклатура "
        "ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Дата) КАК Цены "
        "ПО Цены.Номенклатура = Номенклатура.Ссылка ГДЕ Номенклатура.ПометкаУдаления = ЛОЖЬ"
    )
    monkeypatch.setattr(adapter_server, "metadata_object_form_details", lambda payload: {"status": "ok", "forms": [{
        "name": "ФормаСписка", "elements": [], "commands": [], "dynamic_lists": [],
        "attributes": [{"name": "Список", "dynamic_list_settings": {"query_text": query},
                        "dynamic_list_fields": [{"name": "Цена"}]}],
    }]})

    result = adapter_server.form_field_lineage({
        "base_id": "upo_test", "ref": "Document.ПланированиеМеню", "field": "Цена",
        "match_mode": "exact", "include_call_chain": False,
        "include_calculation": False, "include_binding_inference": False,
    })

    item = result["matches"][0]["item"]
    assert item["source_references"] == [{
        "alias": "Цены", "field": "Цена", "source": "РегистрСведений.ЦеныНоменклатуры.СрезПоследних",
        "public_path": "РегистрСведений.ЦеныНоменклатуры.СрезПоследних.Цена",
    }]
    assert item["query_context"]["joins"] == [{
        "kind": "левое", "source": "РегистрСведений.ЦеныНоменклатуры.СрезПоследних",
        "alias": "Цены", "condition": "Цены.Номенклатура = Номенклатура.Ссылка",
    }]
    assert item["query_context"]["virtual_sources"] == [{
        "alias": "Цены", "source": "РегистрСведений.ЦеныНоменклатуры.СрезПоследних",
        "register": "РегистрСведений.ЦеныНоменклатуры", "virtual_table": "СрезПоследних", "arguments": "&Дата",
    }]


def test_form_field_lineage_lists_each_union_branch_source(monkeypatch: pytest.MonkeyPatch) -> None:
    query = (
        "ВЫБРАТЬ Основные.Цена КАК Цена ИЗ Справочник.Номенклатура КАК Основные "
        "ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ Акционные.ЦенаАкции КАК Цена "
        "ИЗ РегистрСведений.АкционныеЦены КАК Акционные"
    )
    monkeypatch.setattr(adapter_server, "metadata_object_form_details", lambda payload: {"status": "ok", "forms": [{
        "name": "ФормаСписка", "elements": [], "commands": [], "dynamic_lists": [],
        "attributes": [{"name": "Список", "dynamic_list_settings": {"query_text": query},
                        "dynamic_list_fields": [{"name": "Цена"}]}],
    }]})

    result = adapter_server.form_field_lineage({
        "base_id": "upo_test", "ref": "Document.ПланированиеМеню", "field": "Цена",
        "match_mode": "exact", "include_call_chain": False,
        "include_calculation": False, "include_binding_inference": False,
    })

    lineage = result["matches"][0]["item"]["query_lineage"]
    assert lineage["status"] == "partial"
    assert lineage["diagnostics"] == ["complex_query_not_fully_traced"]
    assert lineage["union_branches"] == [
        {"branch": 1, "status": "confirmed", "expression": "Основные.Цена",
         "source_references": [{"alias": "Основные", "field": "Цена", "source": "Справочник.Номенклатура", "public_path": "Справочник.Номенклатура.Цена"}]},
        {"branch": 2, "status": "confirmed", "expression": "Акционные.ЦенаАкции",
         "source_references": [{"alias": "Акционные", "field": "ЦенаАкции", "source": "РегистрСведений.АкционныеЦены", "public_path": "РегистрСведений.АкционныеЦены.ЦенаАкции"}]},
    ]


def test_form_field_lineage_resolves_simple_temporary_table_projection(monkeypatch: pytest.MonkeyPatch) -> None:
    query = (
        "ВЫБРАТЬ Номенклатура.Цена КАК Цена ПОМЕСТИТЬ ВТЦены "
        "ИЗ Справочник.Номенклатура КАК Номенклатура; "
        "ВЫБРАТЬ Цены.Цена КАК Цена ИЗ ВТЦены КАК Цены"
    )
    monkeypatch.setattr(adapter_server, "metadata_object_form_details", lambda payload: {"status": "ok", "forms": [{
        "name": "ФормаСписка", "elements": [], "commands": [], "dynamic_lists": [],
        "attributes": [{"name": "Список", "dynamic_list_settings": {"query_text": query},
                        "dynamic_list_fields": [{"name": "Цена"}]}],
    }]})

    result = adapter_server.form_field_lineage({
        "base_id": "upo_test", "ref": "Document.ПланированиеМеню", "field": "Цена",
        "match_mode": "exact", "include_call_chain": False,
        "include_calculation": False, "include_binding_inference": False,
    })

    item = result["matches"][0]["item"]
    assert item["query_lineage"]["status"] == "partial"
    assert item["source_references"] == [{
        "alias": "Цены", "field": "Цена", "source": "Справочник.Номенклатура",
        "public_path": "Справочник.Номенклатура.Цена",
        "derived_from": {"temporary_table": "ВТЦены", "field": "Цена"},
        "resolution": "temporary_table_projection",
    }]
    assert item["query_context"]["temporary_tables"] == [{
        "name": "ВТЦены", "fields": [{"field": "Цена", "source": "Справочник.Номенклатура", "source_field": "Цена"}],
    }]


def test_form_field_lineage_confirms_tabular_binding_from_element_name(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "metadata_object_form_details", lambda payload: {"status": "ok", "forms": [{
        "name": "ФормаДокумента", "elements": [{"name": "ПродукцияСебестоимость", "type_name": "Поле ввода"}],
        "attributes": [], "commands": [], "dynamic_lists": [],
    }]})
    monkeypatch.setattr(adapter_server, "metadata_object_attributes", lambda payload: {"status": "ok", "tabular_sections": [{
        "name": "Продукция", "columns": [{"name": "Себестоимость", "synonym": "Себестоимость", "type": {"kind": "number"}}],
    }]})
    monkeypatch.setattr(adapter_server, "code_read", lambda payload: {"status": "not_found"})

    result = adapter_server.form_field_lineage({
        "base_id": "upo_test", "ref": "Document.ПланированиеМеню", "field": "ПродукцияСебестоимость",
        "match_mode": "exact", "include_call_chain": False,
    })

    assert result["matches"][0]["item"]["binding"]["object_member_ref"] == "Объект.Продукция.Себестоимость"
    assert result["matches"][0]["item"]["binding"]["member"]["type"]["kind"] == "number"
    assert result["match_summary"]["binding_inference_count"] == 1


def test_form_field_lineage_confirms_object_attribute_binding_from_element_name(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "metadata_object_form_details", lambda payload: {"status": "ok", "forms": [{
        "name": "ФормаДокумента", "elements": [{"name": "ДатаПлана", "type_name": "Поле ввода"}],
        "attributes": [], "commands": [], "dynamic_lists": [],
    }]})
    monkeypatch.setattr(adapter_server, "metadata_object_attributes", lambda payload: {
        "status": "ok", "tabular_sections": [],
        "attributes": [{"name": "ДатаПлана", "synonym": "Дата плана", "type": {"kind": "date"}}],
    })

    result = adapter_server.form_field_lineage({
        "base_id": "upo_test", "ref": "Document.ПланированиеМеню", "field": "ДатаПлана",
        "match_mode": "exact", "include_call_chain": False, "include_calculation": False,
    })

    binding = result["matches"][0]["item"]["binding"]
    assert binding["object_member_ref"] == "Объект.ДатаПлана"
    assert binding["member"]["type"]["kind"] == "date"
    assert binding["source"] == "element_name + decoded_object_attribute"


def test_form_field_lineage_prioritizes_decoded_form_data_path(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "metadata_object_form_details", lambda payload: {"status": "ok", "forms": [{
        "name": "ФормаДокумента", "elements": [{"name": "ДатаПлана", "path_to_data": "Объект.ДатаПлана"}],
        "attributes": [], "commands": [], "dynamic_lists": [],
    }]})
    monkeypatch.setattr(adapter_server, "metadata_object_attributes", lambda payload: pytest.fail("direct form path must not need name inference"))

    result = adapter_server.form_field_lineage({
        "base_id": "upo_test", "ref": "Document.ПланированиеМеню", "field": "ДатаПлана",
        "match_mode": "exact", "include_call_chain": False, "include_calculation": False,
    })

    item = result["matches"][0]["item"]
    assert item["data_path"] == "Объект.ДатаПлана"
    assert item["binding"] == {
        "status": "confirmed", "object_member_ref": "Объект.ДатаПлана", "source": "decoded_form_data_path",
    }


def test_form_field_lineage_exact_mode_accepts_logical_form_data_path(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "metadata_object_form_details", lambda payload: {"status": "ok", "forms": [{
        "name": "ФормаДокумента", "elements": [{"name": "ДатаПлана", "path_to_data": "Объект.ДатаПлана"}],
        "attributes": [], "commands": [], "dynamic_lists": [],
    }]})

    result = adapter_server.form_field_lineage({
        "base_id": "upo_test", "ref": "Document.ПланированиеМеню", "field": "Объект.ДатаПлана",
        "match_mode": "exact", "include_call_chain": False, "include_calculation": False,
    })

    assert result["status"] == "ok"
    assert result["matches"][0]["match_by"] == "exact"
    assert result["matches"][0]["item"]["binding"]["object_member_ref"] == "Объект.ДатаПлана"


def test_form_field_lineage_contract_exposes_precise_and_lightweight_modes() -> None:
    schema = adapter_server.adapter_method_registry()["form.field.lineage"]["input_schema"]

    assert schema["required"] == ["base_id", "field"]
    assert schema["properties"]["match_mode"]["enum"] == ["contains", "exact", "semantic"]
    assert schema["properties"]["include_calculation"]["type"] == "boolean"
    assert schema["properties"]["call_chain_depth"]["maximum"] == 3


def test_public_sanitizer_keeps_logical_form_data_path_but_hides_storage_path() -> None:
    public = adapter_server.sanitize_public_result({
        "data_path": "Объект.ДатаПлана",
        "storage_path": "Config/9/0",
    })

    assert public == {"data_path": "Объект.ДатаПлана"}


def test_public_sanitizer_keeps_followup_request_but_hides_internal_payload() -> None:
    public = adapter_server.sanitize_public_result({
        "follow_up": [{
            "method": "metadata.code_index.search",
            "request": {"base_id": "upo_test", "ref": "Document.СписаниеЗапасов"},
            "payload": {"internal": "must_not_escape"},
        }],
    })

    assert public["follow_up"][0]["request"]["ref"] == "Document.СписаниеЗапасов"
    assert "payload" not in public["follow_up"][0]


def test_form_object_data_paths_are_resolved_from_metadata_identity_guids() -> None:
    order_guid = "21e8237e-5d20-4726-8712-3d43bfc9993e"
    author_guid = "977014d5-3fac-4bdf-9d20-81b437053042"
    profile = {
        "items": [
            {
                "name": "ЗапасыЗаказ",
                "path_to_data": "Объект.Запасы.Заказ",
                "guids_sample": [order_guid],
                "semantic": {"groups": {"Основные": [{"name": "ПутьКДанным", "value": "Объект.Запасы.Заказ"}]}},
            },
            {
                "name": "АвторДокумента",
                "path_to_data": "Объект.АвторДокумента",
                "guids_sample": [author_guid],
                "semantic": {"groups": {"Основные": [{"name": "ПутьКДанным", "value": "Объект.АвторДокумента"}]}},
            },
            {
                "name": "ЗапасыЗаказИтог",
                "path_to_data": "Объект.Запасы.TotalЗаказ",
                "guids_sample": [order_guid],
                "semantic": {"groups": {"Основные": [{"name": "ПутьКДанным", "value": "Объект.Запасы.TotalЗаказ"}]}},
            },
        ]
    }
    object_fields = {
        "attributes": [{"name": "Автор", "identity": {"guid": author_guid}}],
        "tabular_sections": [
            {"name": "Запасы", "columns": [{"name": "ЗаказПокупателя", "identity": {"guid": order_guid}}]}
        ],
    }

    assert adapter_server.enrich_form_profile_object_data_paths(profile, object_fields) == 3
    assert profile["items"][0]["path_to_data"] == "Объект.Запасы.ЗаказПокупателя"
    assert profile["items"][1]["path_to_data"] == "Объект.Автор"
    assert profile["items"][2]["path_to_data"] == "Объект.Запасы.TotalЗаказПокупателя"
    for item in profile["items"]:
        prop = item["semantic"]["groups"]["Основные"][0]
        assert prop["value"] == item["path_to_data"]
        assert prop["source"] == "object_metadata_identity_guid"


def test_preferred_form_payload_file_name_selects_dot_zero_body() -> None:
    guid = "f24e223f-bd30-4d19-b0a2-d1b63eeed65e"

    selected = adapter_server.preferred_form_payload_file_name(
        guid,
        [guid, f"{guid}.1", f"{guid}.0"],
    )

    assert selected == f"{guid}.0"
    assert adapter_server.preferred_form_payload_file_name(guid, [guid]) == ""


def test_form_command_reference_exposes_metadata_guid() -> None:
    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    guid = "64a59186-ea83-44d7-b433-bf93778db6f9"
    node = seq(atom(31), seq(), atom(0), atom(0), atom(0), atom("Кнопка"), seq(), atom(1), seq(atom(0), atom(guid)))

    from parser.form_payload import form_command_reference

    reference = form_command_reference(node, "1.2")

    assert reference == {"guid": guid, "code": "0", "path": "1.2.8", "scope": "metadata", "status": "identity_pending"}


def test_form_command_reference_resolves_known_standard_command() -> None:
    from parser.form_payload import form_command_reference

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    node = seq(atom(31), seq(), atom(0), atom(0), atom(0), atom("Справка"), seq(), atom(1), seq(atom(0), atom("39bb0fe9-771d-4dd5-8a6e-2d16984523af")))

    reference = form_command_reference(node, "1.2")

    assert reference is not None
    assert reference["command_name"] == "Form.StandardCommand.Help"
    assert reference["scope"] == "standard"
    assert reference["status"] == "ok"


def test_form_command_reference_resolves_local_form_command_id() -> None:
    from parser.form_payload import form_command_reference

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    node = seq(atom(31), seq(), atom(0), atom(0), atom(0), atom("Кнопка"), seq(), atom(1), seq(atom(39), atom("409b9a53-7f7e-4178-86c1-33176c7c7a7a")))

    reference = form_command_reference(node, "1.2")

    assert reference is not None
    assert reference["command_id"] == "39"
    assert reference["scope"] == "form"
    assert reference["match_by"] == "form_command_id"


def test_enrich_item_data_paths_resolves_command_button_standard_ref() -> None:
    from parser.form_payload import enrich_item_data_paths

    item = {
        "marker": "31",
        "name": "СвязанныеДокументы",
        "data_path_attribute_id": "1",
        "data_path_field_id": "-5",
        "semantic": {"groups": {}},
    }
    attributes = [{"id": "1", "name": "Объект"}]

    enrich_item_data_paths([item], attributes)

    assert item["path_to_data"] == "Объект.Ref"
    prop = item["semantic"]["groups"]["Основные"][-1]
    assert prop == {"name": "ПутьКДанным", "value": "Объект.Ref", "source": "standard_data_field_reference", "status": "ok"}


def test_command_button_dynamic_field_uses_current_data_path() -> None:
    from parser.form_payload import enrich_item_data_paths

    item = {"marker": "31", "name": "Выполнить", "data_path_attribute_id": "1", "data_path_field_id": "132", "semantic": {"groups": {}}}
    attribute = {
        "id": "1",
        "name": "Список",
        "dynamic_list_settings": {"status": "ok"},
        "dynamic_list_fields": [{"id": "132", "name": "Ссылка", "data_name": "Ref", "path_to_data": "Список.Ref"}],
    }

    enrich_item_data_paths([item], [attribute])

    assert item["path_to_data"] == "Items.Список.CurrentData.Ref"


def test_enrich_item_data_paths_distinguishes_document_number_from_catalog_code() -> None:
    from parser.form_payload import enrich_item_data_paths

    item = {
        "marker": "37",
        "name": "Номер",
        "data_path_attribute_id": "1",
        "data_path_field_id": "-2",
        "semantic": {"groups": {}},
    }

    enrich_item_data_paths([item], [{"id": "1", "name": "Объект"}])

    assert item["path_to_data"] == "Объект.Number"


def test_enrich_item_data_paths_distinguishes_object_date_from_description() -> None:
    from parser.form_payload import enrich_item_data_paths

    item = {
        "marker": "37",
        "name": "Дата",
        "data_path_attribute_id": "1",
        "data_path_field_id": "-3",
        "semantic": {"groups": {}},
    }

    enrich_item_data_paths([item], [{"id": "1", "name": "Объект"}])

    assert item["path_to_data"] == "Объект.Date"


def test_element_event_guids_decode_to_public_event_names() -> None:
    from parser.form_payload import event_handlers_from_node

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    node = seq(
        atom(2),
        atom("fe115cc8-9e33-4684-a166-bd5136fe7a9f"),
        atom("ПолеПриИзменении"),
        atom("f72043b8-2d79-414e-bc4e-3972fe9dbca1"),
        atom("ПолеОбработкаВыбора"),
    )

    events = event_handlers_from_node(node, "1.2", owner="Поле")

    assert [(event["event_name"], event["handler"]) for event in events] == [
        ("OnChange", "ПолеПриИзменении"),
        ("ChoiceProcessing", "ПолеОбработкаВыбора"),
    ]


@pytest.mark.parametrize(
    ("guid", "expected"),
    [
        ("ac5a9c5a-5f1d-4fc5-b88c-a187038c16d1", "Opening"),
        ("70636369-514c-4662-977e-1c3976c9756c", "Tuning"),
        ("650da4af-3233-4ce0-a1ae-23f87a226eee", "DetailProcessing"),
        ("509eca20-d6e4-4fef-a0f8-3a6b44c64178", "URLProcessing"),
        ("3c3da18f-fc18-4f77-8c2d-96c25bec40a5", "Selection"),
        ("1952a54f-35ad-4928-902f-df212ab38ca3", "OnSaveDataInSettingsAtServer"),
        ("93dfba16-26db-46f8-acb5-4f92f50c855f", "NavigationProcessing"),
    ],
)
def test_specialized_element_event_guids_decode_to_public_names(guid: str, expected: str) -> None:
    from parser.form_payload import event_handlers_from_node

    node = parse_brace_text('{1,"' + guid + '","Обработчик"}')

    events = event_handlers_from_node(node, "1.2", owner="Поле")

    assert events[0]["event_name"] == expected


def test_form_xml_event_profile_preserves_handler(tmp_path: Path) -> None:
    form_xml = tmp_path / "Form.xml"
    form_xml.write_text('<Form><Events><Event name="OnChange">ПолеПриИзменении</Event></Events></Form>', encoding="utf-8")

    profile = decode_form_xml(form_xml)

    assert profile["form"]["kind_ru"] == "Форма"
    event = next(item for item in profile["items"] if item["name"] == "OnChange")
    assert event["kind_ru"] == "Событие"
    event = next(item for item in profile["items"] if item["kind"] == "Event")
    properties = [prop for group in event["semantic"]["groups"].values() for prop in group]

    assert next(prop for prop in properties if prop["name"] == "Обработчик")["value"] == "ПолеПриИзменении"


def test_form_common_semantic_decodes_paired_show_command_bar_probe() -> None:
    false_semantic = form_common_semantic(
        [
            {"index": 17, "presentation": "Параметр 17", "value": "0", "value_kind": "boolean_or_number"},
            {"index": 56, "presentation": "Параметр 56", "value": "0", "value_kind": "boolean_or_number"},
        ]
    )
    true_semantic = form_common_semantic(
        [
            {"index": 17, "presentation": "Параметр 17", "value": "2", "value_kind": "number"},
            {"index": 56, "presentation": "Параметр 56", "value": "1", "value_kind": "boolean_or_number"},
        ]
    )
    bottom_semantic = form_common_semantic(
        [
            {"index": 17, "presentation": "Параметр 17", "value": "3", "value_kind": "number"},
            {"index": 56, "presentation": "Параметр 56", "value": "1", "value_kind": "boolean_or_number"},
        ]
    )

    false_property = false_semantic["groups"]["Основные"][0]
    true_property = true_semantic["groups"]["Основные"][0]
    assert false_property["name"] == "ОтображатьКоманднуюПанель"
    assert false_property["value"] is False
    assert false_property["parameter_indices"] == [17, 56]
    assert false_property["write_shape"] == "paired_scalar"
    assert true_property["value"] is True
    assert true_property["parameter_values"] == ["2", "1"]
    assert bottom_semantic["groups"]["Основные"][0]["value"] is True
    assert true_semantic["coverage"] == {"mapped": 2, "unmapped": 0, "total": 2, "status": "ok"}

    enrich_form_common_semantic(
        true_semantic,
        [{"id": "-1", "name": "ФормаКоманднаяПанель", "type_name": "Командная панель"}],
    )
    assert true_semantic["groups"]["Основные"][-1] == {
        "name": "АвтоКоманднаяПанель",
        "value": "ФормаКоманднаяПанель",
        "source": "form_item_reference:id=-1",
        "status": "ok",
    }


def test_form_common_semantic_decodes_command_bar_location_probe() -> None:
    def decode(primary: str, companion: str) -> dict[str, object]:
        semantic = form_common_semantic(
            [
                {"index": 17, "presentation": "Параметр 17", "value": primary, "value_kind": "number"},
                {"index": 56, "presentation": "Параметр 56", "value": companion, "value_kind": "boolean_or_number"},
            ]
        )
        return next(prop for prop in semantic["groups"]["Основные"] if prop["name"] == "ПоложениеКоманднойПанели")

    assert decode("0", "0")["value"] == "None"
    assert decode("2", "1")["value"] == "Top"
    bottom = decode("3", "1")
    assert bottom == {
        "name": "ПоложениеКоманднойПанели",
        "value": "Bottom",
        "source": "controlled_designer_form_command_bar_location",
        "status": "ok",
        "parameter_indices": [17, 56],
        "parameter_values": ["3", "1"],
        "write_shape": "paired_scalar_shared",
    }


def test_form_common_semantic_decodes_document_typed_root_properties() -> None:
    semantic = form_common_semantic(
        [
            {
                "index": 20,
                "typed_kind": "enum",
                "typed_guid": "adeb08a0-415c-11d6-b9d1-0050bae0a95d",
                "typed_value": "3",
            },
            {
                "index": 22,
                "typed_kind": "enum",
                "typed_guid": "20d89b09-bd04-4304-a8c7-4d07fac6338a",
                "typed_value": "3",
            },
            {"index": 24, "typed_kind": "boolean", "typed_value": "1"},
        ]
    )

    props = {item["name"]: item["value"] for items in semantic["groups"].values() for item in items}
    assert props["AutoTime"] == "CurrentOrLast"
    assert props["UsePostingMode"] == "Auto"
    assert props["RepostOnWrite"] is True


def test_form_common_semantic_decodes_paired_window_opening_mode_probe() -> None:
    dont_block = form_common_semantic(
        [
            {"index": 2, "presentation": "Параметр 2", "value": "0", "value_kind": "boolean_or_number"},
            {"index": 54, "presentation": "Параметр 54", "value": "0", "value_kind": "boolean_or_number"},
        ]
    )
    lock_owner = form_common_semantic(
        [
            {"index": 2, "presentation": "Параметр 2", "value": "1", "value_kind": "boolean_or_number"},
            {"index": 54, "presentation": "Параметр 54", "value": "1", "value_kind": "boolean_or_number"},
        ]
    )
    lock_whole_interface = form_common_semantic(
        [
            {"index": 2, "presentation": "Параметр 2", "value": "2", "value_kind": "number"},
            {"index": 54, "presentation": "Параметр 54", "value": "2", "value_kind": "number"},
        ]
    )

    dont_block_property = dont_block["groups"]["Основные"][0]
    lock_owner_property = lock_owner["groups"]["Основные"][0]
    assert dont_block_property == {
        "name": "РежимОткрытияОкна",
        "value": "DontBlock",
        "source": "controlled_designer_form_window_opening_mode",
        "status": "ok",
        "parameter_indices": [2, 54],
        "parameter_values": ["0", "0"],
        "write_shape": "paired_scalar",
    }
    assert lock_owner_property["value"] == "LockOwner"
    assert lock_owner_property["parameter_values"] == ["1", "1"]
    assert lock_whole_interface["groups"]["Основные"][0]["value"] == "LockWholeInterface"
    assert lock_whole_interface["groups"]["Основные"][0]["parameter_values"] == ["2", "2"]
    assert lock_owner["coverage"] == {"mapped": 2, "unmapped": 0, "total": 2, "status": "ok"}


def test_form_common_semantic_decodes_auto_save_data_in_settings_probe() -> None:
    use = form_common_semantic(
        [{"index": 7, "presentation": "Параметр 7", "value": "1", "value_kind": "boolean_or_number"}]
    )
    dont_use = form_common_semantic(
        [{"index": 7, "presentation": "Параметр 7", "value": "0", "value_kind": "boolean_or_number"}]
    )

    use_property = use["groups"]["Основные"][0]
    dont_use_property = dont_use["groups"]["Основные"][0]
    assert use_property == {
        "name": "АвтоСохранениеДанныхВНастройках",
        "value": "Use",
        "source": "controlled_designer_form_auto_save_data_in_settings",
        "status": "ok",
        "parameter_indices": [7],
        "parameter_values": ["1"],
        "write_shape": "scalar_enum",
    }
    assert dont_use_property["value"] == "DontUse"
    assert dont_use["coverage"] == {"mapped": 1, "unmapped": 0, "total": 1, "status": "ok"}


def test_form_common_semantic_decodes_composite_root_group_probe() -> None:
    def parameters(values: str | tuple[str, str, str, str]) -> list[dict[str, object]]:
        resolved_values = (values, values, values, values) if isinstance(values, str) else values
        return [
            {"index": index, "presentation": f"Параметр {index}", "value": value, "value_kind": "boolean_or_number"}
            for index, value in zip((11, 40, 47, 57), resolved_values, strict=True)
        ]

    vertical = form_common_semantic(parameters("0"))
    horizontal = form_common_semantic(parameters("1"))
    always_horizontal = form_common_semantic(parameters(("1", "1", "3", "3")))
    horizontal_if_possible = form_common_semantic(parameters(("1", "2", "2", "2")))

    vertical_property = vertical["groups"]["Основные"][0]
    horizontal_property = horizontal["groups"]["Основные"][0]
    assert vertical_property == {
        "name": "Группировка",
        "value": "Vertical",
        "source": "controlled_designer_form_root_group",
        "status": "ok",
        "parameter_indices": [11, 40, 47, 57],
        "parameter_values": ["0", "0", "0", "0"],
        "write_shape": "composite_scalar",
    }
    assert horizontal_property["value"] == "Horizontal"
    assert horizontal_property["parameter_values"] == ["1", "1", "1", "1"]
    assert always_horizontal["groups"]["Основные"][0]["value"] == "AlwaysHorizontal"
    assert horizontal_if_possible["groups"]["Основные"][0]["value"] == "HorizontalIfPossible"
    assert horizontal["coverage"] == {"mapped": 4, "unmapped": 0, "total": 4, "status": "ok"}


def test_form_common_semantic_decodes_compact_base_form_root_layout() -> None:
    semantic = form_common_semantic(
        [
            {"index": 0, "presentation": "Версия/тип формы", "value": "49", "value_kind": "number"},
            {"index": 2, "presentation": "Параметр 2", "value": "0", "value_kind": "boolean_or_number"},
            {"index": 11, "presentation": "Параметр 11", "value": "0", "value_kind": "boolean_or_number"},
        ]
    )
    properties = semantic["groups"]["Основные"]

    window = next(prop for prop in properties if prop["name"] == "РежимОткрытияОкна")
    group = next(prop for prop in properties if prop["name"] == "Группировка")
    assert window["value"] == "DontBlock"
    assert window["parameter_values"] == ["0", ""]
    assert group["value"] == "Vertical"
    assert group["parameter_values"] == ["0", "", "", ""]


def test_form_common_semantic_decodes_versioned_root_visibility_scroll_and_title() -> None:
    dp = form_common_semantic(
        [
            {"index": 0, "presentation": "Версия/тип формы", "value": "50", "value_kind": "number"},
            {"index": 9, "presentation": "Параметр 9", "value": "1", "value_kind": "boolean_or_number"},
            {"index": 17, "presentation": "Параметр 17", "value": "0", "value_kind": "boolean_or_number"},
        ]
    )
    register = form_common_semantic(
        [
            {"index": 0, "presentation": "Версия/тип формы", "value": "49", "value_kind": "number"},
            {"index": 36, "presentation": "Параметр 36", "value": "2", "value_kind": "number"},
        ]
    )
    report = form_common_semantic(
        [
            {"index": 0, "presentation": "Версия/тип формы", "value": "50", "value_kind": "number"},
            {"index": 9, "presentation": "Параметр 9", "value": "0", "value_kind": "boolean_or_number"},
        ]
    )

    dp_props = {p["name"]: p["value"] for group in dp["groups"].values() for p in group}
    register_props = {p["name"]: p["value"] for group in register["groups"].values() for p in group}
    report_props = {p["name"]: p["value"] for group in report["groups"].values() for p in group}
    assert dp_props["ОтображатьКоманднуюПанель"] is False
    assert dp_props["ПоложениеКоманднойПанели"] == "None"
    assert register_props["VerticalScroll"] == "useIfNecessary"
    assert report_props["AutoTitle"] is False


def test_form_common_semantic_decodes_typed_use_for_folders_and_items() -> None:
    from parser.form_payload import form_common_parameters

    values = ["0"] * 21
    values[0] = "49"
    values[20] = '{"#",59ef2b80-c86b-11d5-a3c1-0050bae0a776,0}'
    tree = parse_brace_text("{4,{" + ",".join(values) + "}}")
    parameters = form_common_parameters(tree)

    semantic = form_common_semantic(parameters)
    prop = next(prop for prop in semantic["groups"]["Основные"] if prop["name"] == "UseForFoldersAndItems")
    assert prop["value"] == "Items"
    assert prop["parameter_index"] == 20
    assert prop["write_shape"] == "typed_enum_read_only"


def test_form_container_type_codes_have_public_names() -> None:
    from parser.form_payload import form_item_public_type_name, item_type_name

    assert item_type_name("22", "0") == "Командная панель"
    assert item_type_name("22", "1") == "Подменю"
    assert item_type_name("22", "2") == "Группа колонок"
    assert item_type_name("22", "3") == "Страницы"
    assert item_type_name("22", "6") == "Группа кнопок"
    assert item_type_name("37", "3") == "Поле флажка"
    assert item_type_name("37", "4") == "PictureField"
    assert item_type_name("37", "5") == "Поле переключателя"
    assert item_type_name("37", "6") == "Поле табличного документа"
    assert form_item_public_type_name("12", "0", "Остаток") == "Декорация надписи"
    assert form_item_public_type_name("12", "1", "Картинка") == "Декорация картинки"
    assert form_item_public_type_name("12", "0", "ПолеРасширеннаяПодсказка") == "Расширенная подсказка"


def test_command_button_representation_decodes_all_enum_values() -> None:
    from parser.form_payload import button_representation_presentation

    assert [button_representation_presentation(str(code)) for code in range(4)] == [
        "Text",
        "Picture",
        "PictureAndText",
        "Авто",
    ]


def test_input_field_title_location_decodes_confirmed_enum_values_only() -> None:
    from parser.form_payload import title_location_presentation

    assert title_location_presentation("0") == "None"
    assert title_location_presentation("1") == "Auto"
    assert title_location_presentation("2") == "Left"
    assert title_location_presentation("3") == "Top"
    assert title_location_presentation("4") == "Right"
    assert title_location_presentation("5") is None


def test_container_type_implies_confirmed_representation() -> None:
    from parser.form_payload import enrich_container_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    node = seq(atom(22), atom(0), atom(0), atom(0), atom(0), atom(6), atom("Группа"), atom(0), atom(0), atom(0), atom(1))
    row = {"marker": "22", "type_name": "Группа кнопок", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_container_specific_semantics(row, node)

    assert row["semantic"]["groups"]["Оформление"][-1] == {
        "name": "Отображение",
        "value": "Compact",
        "source": "form_payload_container_type",
        "status": "ok",
    }


def test_group_layout_exposes_confirmed_stretch_flags() -> None:
    from parser.form_payload import enrich_container_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(30)]
    values[0] = atom(22)
    values[5] = atom(5)
    values[6] = atom("Группа")
    values[10] = atom(1)
    values[14] = atom(1)
    values[15] = atom(0)
    node = seq(*values)
    row = {"marker": "22", "type_name": "Группа", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_container_specific_semantics(row, node)

    props = {prop["name"]: prop["value"] for prop in row["semantic"]["groups"]["Расположение"]}
    assert props["РастягиватьПоГоризонтали"] is True
    assert props["РастягиватьПоВертикали"] is False


def test_page_type_exposes_confirmed_layout_defaults() -> None:
    from parser.form_payload import enrich_container_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    node = seq(atom(22), atom(0), atom(0), atom(0), atom(0), atom(4), atom("Страница"), atom(0), atom(0), atom(0), atom(1))
    row = {"marker": "22", "type_name": "Страница", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_container_specific_semantics(row, node)

    assert row["semantic"]["groups"]["Расположение"][-1]["value"] == "Vertical"
    assert row["semantic"]["groups"]["Использование"][-1]["value"] is False


@pytest.mark.parametrize(
    ("code", "expected"),
    [("0", "None"), ("1", "TabsOnTop")],
)
def test_pages_type_decodes_confirmed_representation(code: str, expected: str) -> None:
    from parser.form_payload import enrich_container_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(21)]
    values[0] = atom(22)
    values[5] = atom(3)
    values[6] = atom("Страницы")
    values[10] = atom(1)
    values[20] = seq(atom(4), atom(code), seq(), atom(2), atom(0), atom(code))
    row = {"marker": "22", "type_name": "Страницы", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_container_specific_semantics(row, seq(*values))

    assert row["semantic"]["groups"]["Оформление"][-1] == {
        "name": "PagesRepresentation",
        "value": expected,
        "source": "form_payload_pages_layout",
        "status": "ok",
        "parameter_index": 20,
    }


def test_pages_type_does_not_guess_representation_when_repeated_codes_disagree() -> None:
    from parser.form_payload import enrich_container_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(21)]
    values[0] = atom(22)
    values[5] = atom(3)
    values[6] = atom("Страницы")
    values[20] = seq(atom(4), atom(1), seq(), atom(2), atom(0), atom(0))
    row = {"marker": "22", "type_name": "Страницы", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_container_specific_semantics(row, seq(*values))

    assert "Оформление" not in row["semantic"]["groups"]


@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True)])
def test_input_field_decodes_confirmed_default_item(code: str, expected: bool) -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(39)]
    values[0] = atom(37)
    values[5] = atom(1)
    values[6] = atom("Дата")
    values[13] = atom(1)
    values[16] = atom(code)
    row = {"marker": "37", "type_name": "Поле надписи", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    prop = next(item for item in row["semantic"]["groups"]["Основные"] if item["name"] == "АктивизироватьПоУмолчанию")
    assert prop == {
        "name": "АктивизироватьПоУмолчанию",
        "value": expected,
        "source": "form_payload_input_field",
        "status": "ok",
        "parameter_index": 16,
    }


@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True)])
def test_input_field_decodes_confirmed_choice_list_button(code: str, expected: bool) -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(40)]
    values[0] = atom(37)
    values[5] = atom(2)
    values[6] = atom("Поле")
    values[7] = atom(0)
    values[13] = atom(1)
    specific = [atom(0) for _ in range(56)]
    specific[0] = atom(36)
    specific[11] = atom(code)
    values[39] = seq(*specific)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    prop = next(item for item in row["semantic"]["groups"]["Использование"] if item["name"] == "ChoiceListButton")
    assert prop == {
        "name": "ChoiceListButton",
        "value": expected,
        "source": "form_payload_input_field",
        "status": "ok",
        "parameter_index": 11,
    }


@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True), ("2", "Авто")])
def test_input_field_decodes_confirmed_open_button(code: str, expected: bool | str) -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(40)]
    values[0] = atom(37)
    values[5] = atom(2)
    values[6] = atom("Поле")
    specific = [atom(0) for _ in range(56)]
    specific[0] = atom(36)
    specific[15] = atom(code)
    values[39] = seq(*specific)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    prop = next(item for item in row["semantic"]["groups"]["Использование"] if item["name"] == "OpenButton")
    assert prop["value"] == expected
    assert prop["parameter_index"] == 15


def test_input_field_open_button_is_independent_from_quick_choice() -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(40)]
    values[0] = atom(37)
    specific = [atom(0) for _ in range(56)]
    specific[0] = atom(36)
    specific[15] = atom(1)
    specific[23] = atom(0)
    specific[32] = atom(0)
    values[39] = seq(*specific)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    props = {item["name"]: item["value"] for item in row["semantic"]["groups"]["Использование"]}
    assert props["OpenButton"] is True
    assert props["БыстрыйВыбор"] is False


@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True), ("2", "Авто")])
def test_input_field_decodes_confirmed_quick_choice(code: str, expected: bool | str) -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(40)]
    values[0] = atom(37)
    specific = [atom(0) for _ in range(56)]
    specific[0] = atom(36)
    specific[23] = atom(code)
    specific[32] = atom("0" if code == "1" else "1")
    values[39] = seq(*specific)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    prop = next(item for item in row["semantic"]["groups"]["Использование"] if item["name"] == "БыстрыйВыбор")
    assert prop["value"] == expected
    assert prop["parameter_index"] == 23


def test_input_field_decodes_quick_choice_independently_from_choose_type() -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(40)]
    values[0] = atom(37)
    specific = [atom(0) for _ in range(56)]
    specific[0] = atom(36)
    specific[23] = atom(1)
    specific[32] = atom(0)
    values[39] = seq(*specific)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    props = {item["name"]: item for item in row["semantic"]["groups"]["Использование"]}
    assert props["БыстрыйВыбор"]["value"] is True
    assert props["БыстрыйВыбор"]["parameter_index"] == 23
    assert props["ВыбиратьТип"]["value"] is False
    assert props["ВыбиратьТип"]["parameter_index"] == 32


@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True)])
def test_input_field_decodes_confirmed_create_button(code: str, expected: bool) -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(40)]
    values[0] = atom(37)
    values[5] = atom(2)
    values[6] = atom("Поле")
    specific = [atom(0) for _ in range(56)]
    specific[0] = atom(36)
    specific[45] = atom(code)
    values[39] = seq(*specific)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    prop = next(item for item in row["semantic"]["groups"]["Использование"] if item["name"] == "CreateButton")
    assert prop == {
        "name": "CreateButton",
        "value": expected,
        "source": "form_payload_input_field",
        "status": "ok",
        "parameter_index": 45,
    }


@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True), ("2", "Авто")])
def test_input_field_decodes_confirmed_clear_button(code: str, expected: bool | str) -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(40)]
    values[0] = atom(37)
    values[5] = atom(2)
    specific = [atom(0) for _ in range(56)]
    specific[0] = atom(36)
    specific[13] = atom(code)
    values[39] = seq(*specific)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    prop = next(item for item in row["semantic"]["groups"]["Использование"] if item["name"] == "КнопкаОчистки")
    assert prop["value"] == expected
    assert prop["parameter_index"] == 13


@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True)])
def test_input_field_decodes_confirmed_text_edit(code: str, expected: bool) -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(40)]
    values[0] = atom(37)
    values[5] = atom(2)
    specific = [atom(0) for _ in range(56)]
    specific[0] = atom(36)
    specific[41] = atom(code)
    specific[55] = atom("0" if code == "1" else "1")
    values[39] = seq(*specific)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    prop = next(item for item in row["semantic"]["groups"]["Использование"] if item["name"] == "РедактированиеТекста")
    assert prop["value"] is expected
    assert prop["parameter_index"] == 41


@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True)])
def test_input_field_decodes_choose_type_from_index_32_not_54(code: str, expected: bool) -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(40)]
    values[0] = atom(37)
    values[5] = atom(2)
    specific = [atom(0) for _ in range(56)]
    specific[0] = atom(36)
    specific[32] = atom(code)
    specific[54] = atom("0" if code == "1" else "1")
    values[39] = seq(*specific)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    prop = next(item for item in row["semantic"]["groups"]["Использование"] if item["name"] == "ВыбиратьТип")
    assert prop["value"] is expected
    assert prop["parameter_index"] == 32


@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True)])
def test_input_field_decodes_confirmed_show_in_header(code: str, expected: bool) -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(40)]
    values[0] = atom(37)
    values[5] = atom(2)
    values[20] = atom(code)
    specific = [atom(0) for _ in range(56)]
    specific[0] = atom(36)
    specific[20] = atom("0" if code == "1" else "1")
    values[39] = seq(*specific)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    prop = next(item for item in row["semantic"]["groups"]["Расположение"] if item["name"] == "ShowInHeader")
    assert prop["value"] is expected
    assert prop["parameter_index"] == 20


@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True)])
def test_input_field_decodes_confirmed_visibility_from_index_43(code: str, expected: bool) -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(44)]
    values[0] = atom(37)
    values[5] = atom(2)
    values[13] = atom("0" if code == "1" else "1")
    values[43] = atom(code)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    prop = next(item for item in row["semantic"]["groups"]["Основные"] if item["name"] == "Видимость")
    assert prop["value"] is expected
    assert prop["parameter_index"] == 43


@pytest.mark.parametrize(("index", "name", "group"), [(21, "ShowInFooter", "Расположение"), (22, "CellHyperlink", "Использование")])
@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True)])
def test_input_field_decodes_confirmed_direct_boolean_properties(
    index: int,
    name: str,
    group: str,
    code: str,
    expected: bool,
) -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(44)]
    values[0] = atom(37)
    values[5] = atom(2)
    values[index] = atom(code)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    prop = next(item for item in row["semantic"]["groups"][group] if item["name"] == name)
    assert prop["value"] is expected
    assert prop["parameter_index"] == index


@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True)])
def test_input_field_decodes_confirmed_read_only(code: str, expected: bool) -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(44)]
    values[0] = atom(37)
    values[5] = atom(2)
    values[14] = atom(code)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    prop = next(item for item in row["semantic"]["groups"]["Основные"] if item["name"] == "ТолькоПросмотр")
    assert prop["value"] is expected
    assert prop["parameter_index"] == 14


@pytest.mark.parametrize(("code", "expected"), [("1", True), ("2", False)])
def test_input_field_decodes_confirmed_skip_on_input(code: str, expected: bool) -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(44)]
    values[0] = atom(37)
    values[5] = atom(2)
    values[15] = atom(code)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    prop = next(item for item in row["semantic"]["groups"]["Использование"] if item["name"] == "ПропускатьПриВводе")
    assert prop["value"] is expected
    assert prop["parameter_index"] == 15


@pytest.mark.parametrize(
    ("index", "name", "group", "code", "expected"),
    [
        (8, "TitleHeight", "Расположение", "0", "0"),
        (8, "TitleHeight", "Расположение", "2", "2"),
        (23, "HorizontalAlign", "Расположение", "0", "Left"),
        (23, "HorizontalAlign", "Расположение", "1", "Center"),
        (23, "HorizontalAlign", "Расположение", "2", "Right"),
        (23, "HorizontalAlign", "Расположение", "3", "Auto"),
        (26, "РежимРедактирования", "Использование", "0", "Directly"),
        (26, "РежимРедактирования", "Использование", "1", "Auto"),
        (26, "РежимРедактирования", "Использование", "2", "EnterOnInput"),
        (53, "GroupHorizontalAlign", "Расположение", "2", "Right"),
        (53, "GroupHorizontalAlign", "Расположение", "3", "Auto"),
    ],
)
def test_input_field_decodes_confirmed_direct_scalar_and_enum_properties(
    index: int,
    name: str,
    group: str,
    code: str,
    expected: str,
) -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(54)]
    values[0] = atom(37)
    values[5] = atom(2)
    values[index] = atom(code)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    prop = next(item for item in row["semantic"]["groups"][group] if item["name"] == name)
    assert prop["value"] == expected
    assert prop["parameter_index"] == index


def test_input_field_derives_auto_edit_mode_from_enter_on_input() -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(59)]
    values[0] = atom(37)
    values[26] = atom(2)
    row = {"marker": "37", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    usage = {item["name"]: item for item in row["semantic"]["groups"]["Использование"]}
    assert usage["РежимРедактирования"]["value"] == "EnterOnInput"
    assert usage["AutoEditMode"]["value"] is True
    assert usage["AutoEditMode"]["parameter_index"] == 26


@pytest.mark.parametrize(
    ("code", "expected"),
    [("0", "None"), ("1", "Auto"), ("2", "Left"), ("3", "Top"), ("4", "Right")],
)
def test_input_field_decodes_all_confirmed_title_locations(code: str, expected: str) -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(44)]
    values[0] = atom(37)
    values[5] = atom(2)
    values[7] = atom(code)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    prop = next(item for item in row["semantic"]["groups"]["Основные"] if item["name"] == "ПоложениеЗаголовка")
    assert prop["value"] == expected
    assert prop["parameter_index"] == 7


@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True)])
def test_input_field_decodes_confirmed_enabled(code: str, expected: bool) -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(54)]
    values[0] = atom(37)
    values[5] = atom(2)
    values[13] = atom(code)
    values[43] = atom("1")
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    prop = next(item for item in row["semantic"]["groups"]["Основные"] if item["name"] == "Доступность")
    assert prop["value"] is expected
    assert prop["parameter_index"] == 13


@pytest.mark.parametrize(("code", "expected"), [("0", "Auto"), ("3", "Button")])
def test_input_field_decodes_confirmed_tooltip_representation(code: str, expected: str) -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(54)]
    values[0] = atom(37)
    values[5] = atom(2)
    values[50] = atom(code)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    prop = next(item for item in row["semantic"]["groups"]["Оформление"] if item["name"] == "ToolTipRepresentation")
    assert prop["value"] == expected
    assert prop["parameter_index"] == 50


@pytest.mark.parametrize(
    ("index", "name", "group", "code", "expected"),
    [
        (4, "HorizontalStretch", "Расположение", "0", False),
        (4, "HorizontalStretch", "Расположение", "1", True),
        (4, "HorizontalStretch", "Расположение", "2", "Авто"),
        (6, "Wrap", "Расположение", "0", False),
        (6, "Wrap", "Расположение", "1", True),
        (19, "ListChoiceMode", "Использование", "0", False),
        (19, "ListChoiceMode", "Использование", "1", True),
        (28, "AutoChoiceIncomplete", "Использование", "1", True),
        (28, "AutoChoiceIncomplete", "Использование", "2", "Авто"),
        (31, "MarkRequiredComplete", "Использование", "0", False),
        (31, "MarkRequiredComplete", "Использование", "1", True),
        (31, "MarkRequiredComplete", "Использование", "2", "Авто"),
        (31, "AutoMarkIncomplete", "Использование", "0", False),
        (31, "AutoMarkIncomplete", "Использование", "1", True),
        (31, "AutoMarkIncomplete", "Использование", "2", "Авто"),
        (52, "ExtendedEdit", "Использование", "0", False),
        (52, "ExtendedEdit", "Использование", "1", True),
        (65, "ExtendedEditMultipleValues", "Использование", "0", False),
        (65, "ExtendedEditMultipleValues", "Использование", "1", True),
    ],
)
def test_input_field_decodes_confirmed_nested_usage_and_layout_properties(
    index: int,
    name: str,
    group: str,
    code: str,
    expected: bool | str,
) -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(40)]
    values[0] = atom(37)
    values[5] = atom(2)
    specific = [atom(0) for _ in range(66)]
    specific[0] = atom(36)
    specific[index] = atom(code)
    values[39] = seq(*specific)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    prop = next(item for item in row["semantic"]["groups"][group] if item["name"] == name)
    assert prop["value"] == expected
    assert prop["parameter_index"] == index


@pytest.mark.parametrize(
    ("index", "name", "code", "expected"),
    [
        (2, "Width", "0", "0"),
        (2, "Width", "42", "42"),
        (3, "Height", "0", "0"),
        (3, "Height", "2", "2"),
        (5, "VerticalStretch", "0", False),
        (5, "VerticalStretch", "1", True),
        (5, "VerticalStretch", "2", "Авто"),
        (21, "ChoiceListHeight", "0", "0"),
        (21, "ChoiceListHeight", "5", "5"),
        (52, "AutoMaxHeight", "0", False),
        (52, "AutoMaxHeight", "1", True),
        (53, "MaxHeight", "0", "0"),
        (53, "MaxHeight", "3", "3"),
    ],
)
def test_input_field_decodes_confirmed_nested_size_properties(
    index: int,
    name: str,
    code: str,
    expected: bool | str,
) -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(40)]
    values[0] = atom(37)
    values[5] = atom(2)
    specific = [atom(0) for _ in range(66)]
    specific[0] = atom(36)
    specific[index] = atom(code)
    values[39] = seq(*specific)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    prop = next(item for item in row["semantic"]["groups"]["Расположение"] if item["name"] == name)
    assert prop["value"] == expected
    assert prop["parameter_index"] == index


@pytest.mark.parametrize(
    ("index", "name", "code", "expected"),
    [
        (8, "MultiLine", "1", True),
        (8, "MultiLine", "2", False),
        (14, "SpinButton", "0", False),
        (14, "SpinButton", "1", True),
        (14, "SpinButton", "2", "Авто"),
        (24, "ChoiceFoldersAndItems", "0", "Items"),
        (24, "ChoiceFoldersAndItems", "3", "Auto"),
        (43, "EditTextUpdate", "0", "Auto"),
        (43, "EditTextUpdate", "2", "OnValueChange"),
        (46, "ChoiceButtonRepresentation", "0", "Auto"),
        (46, "ChoiceButtonRepresentation", "2", "ShowInDropListAndInInputField"),
        (48, "ChoiceHistoryOnInput", "0", "Auto"),
        (48, "ChoiceHistoryOnInput", "1", "DontUse"),
    ],
)
def test_input_field_decodes_confirmed_nested_editing_properties(
    index: int,
    name: str,
    code: str,
    expected: bool | str,
) -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(40)]
    values[0] = atom(37)
    values[5] = atom(2)
    specific = [atom(0) for _ in range(66)]
    specific[0] = atom(36)
    specific[index] = atom(code)
    values[39] = seq(*specific)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    prop = next(item for item in row["semantic"]["groups"]["Использование"] if item["name"] == name)
    assert prop["value"] == expected
    assert prop["parameter_index"] == index


def test_input_field_decodes_structured_style_properties() -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    style_guid = "da164c9a-4836-4c07-ace9-1c2e2382eceb"
    values = [atom(0) for _ in range(40)]
    values[0] = atom(37)
    values[5] = atom(2)
    specific = [atom(0) for _ in range(66)]
    specific[0] = atom(36)
    specific[37] = seq(atom(3), atom(3), seq(atom(-21)))
    specific[38] = seq(atom(3), atom(3), seq(atom(0), atom(style_guid)))
    specific[39] = seq(atom(3), atom(3), seq(atom(-1)))
    specific[40] = seq(atom(7), atom(3), atom(0), atom(1), atom(100))
    values[39] = seq(*specific)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    props = {item["name"]: item for item in row["semantic"]["groups"]["Оформление"]}
    assert props["TextColor"]["value"] == "style:ButtonTextColor"
    assert props["TextColor"]["parameter_index"] == 37
    assert props["BackColor"]["value"] == {"kind": "StyleItem", "guid": style_guid, "status": "identity_pending"}
    assert props["BackColor"]["parameter_index"] == 38
    assert props["BorderColor"]["value"] == "style:FormBackColor"
    assert props["BorderColor"]["parameter_index"] == 39
    assert props["Font"]["value"] == "Авто"
    assert props["Font"]["parameter_index"] == 40


def test_input_field_decodes_standard_and_custom_font_references() -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    style_guid = "fa2a9ef2-00a1-44f4-a82c-6c7288dd62dc"

    def decode(font):
        values = [atom(0) for _ in range(40)]
        values[0] = atom(37)
        values[5] = atom(2)
        specific = [atom(0) for _ in range(66)]
        specific[0] = atom(36)
        specific[40] = font
        values[39] = seq(*specific)
        row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
        enrich_input_field_specific_semantics(row, seq(*values))
        return next(prop["value"] for prop in row["semantic"]["groups"]["Оформление"] if prop["name"] == "Font")

    assert decode(seq(atom(7), atom(2), atom(0), seq(atom(-31)), atom(1), atom(100))) == "style:NormalTextFont"
    assert decode(seq(atom(7), atom(2), atom(0), seq(atom(-32)), atom(1), atom(100))) == "style:LargeTextFont"
    assert decode(seq(atom(7), atom(2), atom(0), seq(atom(0), atom(style_guid)), atom(1), atom(100))) == {
        "kind": "StyleItem",
        "guid": style_guid,
        "status": "identity_pending",
    }


def test_input_field_decodes_localized_formats_and_input_hint() -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    localized = lambda value: seq(atom(1), atom(1), seq(atom("ru"), atom(value)))
    values = [atom(0) for _ in range(40)]
    values[0] = atom(37)
    values[5] = atom(2)
    specific = [atom(0) for _ in range(66)]
    specific[0] = atom(36)
    specific[29] = localized("ЧДЦ=2; ЧН=0,00")
    specific[30] = localized("ДФ=dd.MM.yyyy")
    specific[44] = localized("Введите дату")
    values[39] = seq(*specific)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    formatting = {prop["name"]: prop for prop in row["semantic"]["groups"]["Форматирование"]}
    assert formatting["Format"]["value"] == "ЧДЦ=2; ЧН=0,00"
    assert formatting["Format"]["parameter_index"] == 29
    assert formatting["EditFormat"]["value"] == "ДФ=dd.MM.yyyy"
    assert formatting["EditFormat"]["parameter_index"] == 30
    hint = next(prop for prop in row["semantic"]["groups"]["Оформление"] if prop["name"] == "InputHint")
    assert hint["value"] == "Введите дату"
    assert hint["parameter_index"] == 44


@pytest.mark.parametrize(("code", "expected"), [("1", True), ("2", False)])
def test_input_field_decodes_password_mode(code: str, expected: bool) -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(40)]
    values[0] = atom(37)
    values[5] = atom(2)
    specific = [atom(0) for _ in range(66)]
    specific[0] = atom(36)
    specific[7] = atom(code)
    values[39] = seq(*specific)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    prop = next(prop for prop in row["semantic"]["groups"]["Использование"] if prop["name"] == "PasswordMode")
    assert prop["value"] is expected
    assert prop["parameter_index"] == 7


def test_input_field_decodes_choice_parameter_link_names() -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(40)]
    values[0] = atom(37)
    values[5] = atom(2)
    specific = [atom(0) for _ in range(66)]
    specific[0] = atom(36)
    specific[64] = seq(
        atom(5007),
        atom(2),
        atom("Отбор.Банк"),
        atom(1),
        seq(atom(161)),
        atom(0),
        atom(""),
        atom(""),
        atom("Отбор.Владелец"),
        atom(2),
        seq(atom(1)),
        seq(atom(0), atom("ba7dcb3b-b8b9-4d44-ab5f-56f7a228f10d")),
        atom(0),
        atom(""),
        atom(""),
    )
    values[39] = seq(*specific)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    prop = next(
        prop
        for prop in row["semantic"]["groups"]["Использование"]
        if prop["name"] == "ChoiceParameterLinks"
    )
    assert prop["value"] == {
        "count": 2,
        "links": [{"name": "Отбор.Банк"}, {"name": "Отбор.Владелец"}],
    }
    assert prop["parameter_index"] == 64


def test_input_field_does_not_expose_empty_or_malformed_choice_parameter_links() -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    for links in (seq(atom(5007), atom(0)), seq(atom(5007), atom(2), atom("Отбор.Банк"))):
        values = [atom(0) for _ in range(40)]
        values[0] = atom(37)
        values[5] = atom(2)
        specific = [atom(0) for _ in range(66)]
        specific[0] = atom(36)
        specific[64] = links
        values[39] = seq(*specific)
        row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

        enrich_input_field_specific_semantics(row, seq(*values))

        assert all(
            prop["name"] != "ChoiceParameterLinks"
            for props in row["semantic"]["groups"].values()
            for prop in props
        )


def test_input_field_decodes_mask_and_numeric_choice_list() -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}

    def choice(value: int, presentation: str) -> dict:
        return seq(
            atom("#"),
            atom("0e704aa2-07bd-48b9-8223-a0212c4d5fc2"),
            seq(
                atom(0),
                atom(1),
                seq(atom("N"), atom(value)),
                atom("00000000-0000-0000-0000-000000000000"),
                atom("00000000-0000-0000-0000-000000000000"),
                seq(atom(1), atom(1), seq(atom("ru"), atom(presentation))),
            ),
        )

    values = [atom(0) for _ in range(40)]
    values[0] = atom(37)
    values[5] = atom(2)
    specific = [atom(0) for _ in range(66)]
    specific[0] = atom(36)
    specific[1] = seq(
        atom(3),
        atom(3),
        atom(""),
        choice(2, "Не выполненные"),
        atom(""),
        choice(1, "Выполненные"),
        atom(""),
        choice(0, "Все задачи"),
    )
    specific[18] = atom("999-999")
    values[39] = seq(*specific)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    usage = {prop["name"]: prop for prop in row["semantic"]["groups"]["Использование"]}
    assert usage["Mask"]["value"] == "999-999"
    assert usage["Mask"]["parameter_index"] == 18
    assert usage["ChoiceList"]["value"] == {
        "count": 3,
        "items": [
            {"presentation": "Не выполненные", "value": {"kind": "Number", "value": "2"}},
            {"presentation": "Выполненные", "value": {"kind": "Number", "value": "1"}},
            {"presentation": "Все задачи", "value": {"kind": "Number", "value": "0"}},
        ],
    }
    assert usage["ChoiceList"]["parameter_index"] == 1


def test_input_field_does_not_guess_unsupported_choice_list_value_type() -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(40)]
    values[0] = atom(37)
    values[5] = atom(2)
    specific = [atom(0) for _ in range(66)]
    specific[0] = atom(36)
    specific[1] = seq(atom(3), atom(1), atom(""), seq(atom("#"), atom("guid"), seq(atom(0), atom(1), seq(atom("R"), atom("ref")))))
    values[39] = seq(*specific)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    assert all(
        prop["name"] != "ChoiceList"
        for props in row["semantic"]["groups"].values()
        for prop in props
    )


def test_input_field_decodes_pending_enum_choice_list_reference() -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    type_guid = "e36f8d8f-9d89-433b-bf81-6511e4141522"
    value_guid = "2d31cb6b-c0f4-4554-b5f0-b7343aa22a99"
    encoded_item = seq(
        atom("#"),
        atom("0e704aa2-07bd-48b9-8223-a0212c4d5fc2"),
        seq(atom(0), atom(0), seq(atom("U")), atom(type_guid), atom(value_guid), seq(atom(1), atom(0))),
    )
    values = [atom(0) for _ in range(40)]
    values[0] = atom(37)
    values[5] = atom(5)
    specific = [atom(0) for _ in range(12)]
    specific[0] = atom(8)
    specific[1] = seq(atom(3), atom(1), atom(""), encoded_item)
    values[39] = seq(*specific)
    row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    prop = next(prop for prop in row["semantic"]["groups"]["Использование"] if prop["name"] == "ChoiceList")
    assert prop["value"]["items"][0]["value"] == {
        "kind": "EnumValue",
        "type_guid": type_guid,
        "value_guid": value_guid,
        "status": "identity_pending",
    }


@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True)])
def test_label_field_decodes_confirmed_hiperlink_flag(code: str, expected: bool) -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(40)]
    values[0] = atom(37)
    values[5] = atom(1)
    specific = [atom(0) for _ in range(20)]
    specific[0] = atom(11)
    specific[7] = atom(code)
    values[39] = seq(*specific)
    row = {
        "marker": "37",
        "type_code": "1",
        "type_name": "Поле надписи",
        "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}},
    }

    enrich_input_field_specific_semantics(row, seq(*values))

    prop = next(prop for prop in row["semantic"]["groups"]["Использование"] if prop["name"] == "Hiperlink")
    assert prop["value"] is expected
    assert prop["parameter_index"] == 7


def test_report_label_and_chart_fields_decode_layout_properties() -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    label_values = ["0"] * 40
    label_values[0] = "37"
    label_values[5] = "1"
    label_values[39] = "{11,50,5,2,0,2,0,0}"
    label_row = {"marker": "37", "type_code": "1", "semantic": {"groups": {}}}
    enrich_input_field_specific_semantics(label_row, parse_brace_text("{" + ",".join(label_values) + "}"))

    chart_values = ["0"] * 40
    chart_values[0] = "37"
    chart_values[5] = "11"
    chart_values[39] = "{1,50,8,1,1}"
    chart_row = {"marker": "37", "type_code": "11", "semantic": {"groups": {}}}
    enrich_input_field_specific_semantics(chart_row, parse_brace_text("{" + ",".join(chart_values) + "}"))

    label_props = {prop["name"]: prop["value"] for group in label_row["semantic"]["groups"].values() for prop in group}
    chart_props = {prop["name"]: prop["value"] for group in chart_row["semantic"]["groups"].values() for prop in group}
    assert label_props["Ширина"] == "50"
    assert label_props["Высота"] == "5"
    assert label_props["ВертикальноеПоложениеВГруппе"] == "Top"
    assert label_props["РастягиватьПоВертикали"] is False
    assert chart_props["Высота"] == "8"


def test_graphical_schema_field_decodes_size_and_edit_mode() -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    values = ["0"] * 40
    values[0] = "37"
    values[5] = "14"
    values[39] = "{3,80,25,0,0}"
    row = {"marker": "37", "type_code": "14", "semantic": {"groups": {}}}

    enrich_input_field_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))

    props = {prop["name"]: prop["value"] for group in row["semantic"]["groups"].values() for prop in group}
    assert props["Ширина"] == "80"
    assert props["Высота"] == "25"
    assert props["Edit"] is False


def test_report_styles_decode_tooltip_background_and_web_green() -> None:
    from parser.form_payload import style_value_presentation

    assert style_value_presentation(parse_brace_text("{3,3,{-23}}")) == "style:ToolTipBackColor"
    assert style_value_presentation(parse_brace_text("{3,2,{27}}")) == "web:DarkGreen"


def test_input_field_type_link_is_resolved_to_public_current_data_path() -> None:
    from parser.form_payload import enrich_input_field_specific_semantics, enrich_item_data_paths

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(40)]
    values[0] = atom(37)
    values[5] = atom(2)
    specific = [atom(0) for _ in range(66)]
    specific[0] = atom(36)
    specific[42] = seq(
        atom(3),
        atom(2),
        seq(atom(1), atom("02023637-7868-4a5f-8576-835a76e0c9ba")),
        seq(atom(1)),
        atom(0),
    )
    values[39] = seq(*specific)
    row = {
        "marker": "37",
        "type_code": "2",
        "type_name": "Поле ввода",
        "data_path_attribute_id": "1",
        "data_path_field_id": "2",
        "parameters": [],
        "semantic": {"groups": {}, "unmapped_parameters": [{"index": 42}], "coverage": {"mapped": 0, "unmapped": 1}},
    }

    enrich_input_field_specific_semantics(row, seq(*values))
    enrich_item_data_paths(
        [row],
        [
            {
                "id": "1",
                "name": "ДеревоНастроек",
                "dynamic_list_fields": [
                    {"id": "1", "name": "Настройка", "data_name": "Настройка"},
                    {"id": "2", "name": "Значение", "data_name": "Значение"},
                ],
            }
        ],
    )

    prop = next(prop for prop in row["semantic"]["groups"]["Использование"] if prop["name"] == "TypeLink")
    assert prop["value"] == {
        "path_to_data": "Items.ДеревоНастроек.CurrentData.Настройка",
        "link_item": 0,
    }
    assert prop["parameter_index"] == 42
    assert "_type_link_reference" not in row


def test_footer_data_path_reference_and_object_tabular_path_are_resolved() -> None:
    from parser.form_payload import enrich_item_data_paths, footer_data_path_reference

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    owner_guid = "82363dd2-24bb-4e24-a05a-df6e01759f8f"
    field_guid = "75518f7c-ee35-4bd7-a107-acda05e715c6"
    primary = seq(atom(3), seq(atom(1)), seq(atom(0), atom(owner_guid)), seq(atom(0), atom(field_guid)))
    footer = seq(atom(3), seq(atom(1)), seq(atom(0), atom(owner_guid)), seq(atom(101000000), atom(field_guid)))
    assert footer_data_path_reference(primary, footer) == {"aggregate": "Total", "field_guid": field_guid}

    table = {"marker": "55", "name": "ПозицииЧека", "path": "1", "parameters": [], "semantic": {"groups": {}}}
    field = {
        "marker": "37",
        "name": "ПозицииЧекаКоличество",
        "path": "1.2",
        "data_path_attribute_id": "0",
        "parameters": [],
        "_footer_data_path_reference": {"aggregate": "Total", "field_guid": field_guid},
        "semantic": {"groups": {}, "unmapped_parameters": [{"index": 12}], "coverage": {"mapped": 0, "unmapped": 1}},
    }

    enrich_item_data_paths([table, field], [{"id": "1", "name": "Объект"}])

    assert field["path_to_data"] == "Объект.ПозицииЧека.Количество"
    main = {prop["name"]: prop for prop in field["semantic"]["groups"]["Основные"]}
    assert main["FooterDataPath"]["value"] == "Объект.ПозицииЧека.TotalКоличество"
    assert main["FooterDataPath"]["parameter_index"] == 12
    assert "_footer_data_path_reference" not in field

    aggregate_primary = seq(atom(3), seq(atom(1)), seq(atom(0), atom(owner_guid)), seq(atom(101000000), atom(field_guid)))
    assert footer_data_path_reference(aggregate_primary, seq(atom(0))) == {
        "aggregate": "Total",
        "field_guid": field_guid,
        "primary_aggregate": True,
    }
    total = {
        "marker": "37",
        "name": "ПозицииЧекаКоличествоИтог",
        "path": "1.3",
        "guids_sample": [field_guid],
        "_footer_data_path_reference": {
            "aggregate": "Total",
            "field_guid": field_guid,
            "primary_aggregate": True,
        },
        "semantic": {"groups": {}, "unmapped_parameters": [{"index": 12}], "coverage": {"mapped": 0, "unmapped": 1}},
    }
    field["guids_sample"] = [field_guid]
    table["guids_sample"] = [field_guid]
    enrich_item_data_paths([table, field, total], [{"id": "1", "name": "Объект"}])
    assert total["path_to_data"] == "Объект.ПозицииЧека.TotalКоличество"
    assert next(prop for prop in total["semantic"]["groups"]["Основные"] if prop["name"] == "ПутьКДанным")["value"] == total["path_to_data"]


def test_form_additional_columns_are_decoded_from_attribute_section_tail() -> None:
    from parser.form_payload import additional_column_items

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    owner_guid = "3e998dcc-3dfd-4562-bbb0-7684217acc79"
    owner_reference = seq(atom(2), seq(atom(1)), seq(atom(0), atom(owner_guid)))
    group = seq(
        atom(0),
        owner_reference,
        atom(2),
        seq(atom(5), atom(1), atom(0), atom("Пометка")),
        seq(atom(5), atom(2), atom(0), atom("ИспользоватьХарактеристики")),
    )
    tree = seq(atom(0), atom(0), atom(0), seq(atom(4), atom(1), seq(atom(9)), group))
    owner = {
        "marker": "55",
        "name": "Запасы",
        "path_to_data": "Объект.Запасы",
        "guids_sample": [owner_guid],
    }

    rows, total, truncated = additional_column_items(tree, [owner])

    assert total == 2
    assert truncated is False
    assert [(row["id"], row["name"], row["path_to_data"]) for row in rows] == [
        ("1", "Пометка", "Объект.Запасы.Пометка"),
        ("2", "ИспользоватьХарактеристики", "Объект.Запасы.ИспользоватьХарактеристики"),
    ]


def test_legacy_marker_35_input_field_is_included_with_existing_semantics() -> None:
    from parser.form_payload import item_records

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(57)]
    values[0] = atom(35)
    values[1] = seq(atom(70), atom("02023637-7868-4a5f-8576-835a76e0c9ba"))
    values[5] = atom(2)
    values[6] = atom("ПроксиПароль")
    values[9] = seq(atom(1), atom(1), seq(atom("ru"), atom("Пароль")))
    values[11] = seq(atom(1), seq(atom(13)))
    values[13] = atom(1)
    values[14] = atom(0)
    values[15] = atom(2)
    values[43] = atom(1)
    specific = [atom(0) for _ in range(66)]
    specific[0] = atom(32)
    specific[2] = atom(25)
    specific[7] = atom(1)
    specific[40] = seq(atom(7), atom(3), atom(0), atom(1), atom(100))
    values[39] = seq(*specific)

    items, total, truncated = item_records(seq(atom(0), seq(*values)), limit=10, include_parameters=True)

    assert total == 1
    assert truncated is False
    assert len(items) == 1
    row = items[0]
    assert row["marker"] == "35"
    assert row["marker_name"] == "InputField"
    assert row["name"] == "ПроксиПароль"
    assert row["title"] == "Пароль"
    assert row["type_name"] == "Поле ввода"
    assert row["data_path_attribute_id"] == "13"
    usage = {prop["name"]: prop["value"] for prop in row["semantic"]["groups"]["Использование"]}
    assert usage["PasswordMode"] is True
    layout = {prop["name"]: prop["value"] for prop in row["semantic"]["groups"]["Расположение"]}
    assert layout["Width"] == "25"
    appearance = {prop["name"]: prop["value"] for prop in row["semantic"]["groups"]["Оформление"]}
    assert appearance["Font"] == "Авто"


def test_shifted_legacy_marker_35_input_field_layout_is_decoded() -> None:
    from parser.form_payload import item_records

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(58)]
    values[0] = atom(35)
    values[1] = seq(atom(1), atom("02023637-7868-4a5f-8576-835a76e0c9ba"))
    values[5] = atom("")
    values[6] = atom(2)
    values[7] = atom("Период")
    values[10] = seq(atom(1), atom(1), seq(atom("ru"), atom("Период")))
    values[12] = seq(atom(1), seq(atom(13)))
    values[14] = atom(1)
    values[15] = atom(0)
    values[16] = atom(2)
    values[44] = atom(1)
    specific = [atom(0) for _ in range(66)]
    specific[0] = atom(32)
    specific[2] = atom(25)
    specific[15] = atom(1)
    values[40] = seq(*specific)

    items, total, truncated = item_records(seq(atom(0), seq(*values)), limit=10, include_parameters=True)

    assert total == 1
    assert truncated is False
    row = items[0]
    assert row["name"] == "Период"
    assert row["name_path"].endswith(".7")
    assert row["type_code"] == "2"
    assert row["type_name"] == "Поле ввода"
    assert row["data_path_attribute_id"] == "13"
    main = {prop["name"]: prop["value"] for prop in row["semantic"]["groups"]["Основные"]}
    usage = {prop["name"]: prop["value"] for prop in row["semantic"]["groups"]["Использование"]}
    assert main["Имя"] == "Период"
    assert main["Доступность"] is True
    assert usage["OpenButton"] is True


def test_register_record_legacy_field_data_paths_use_public_item_names() -> None:
    from parser.form_payload import enrich_item_data_paths

    attributes = [{"id": "1", "name": "Запись"}]
    items = [
        {"marker": "35", "id": "1", "name": "Период", "path": "1.1", "data_path_attribute_id": "1", "data_path_field_id": "-2", "parameters": [], "semantic": {"groups": {}}},
        {"marker": "35", "id": "3", "name": "СтруктурнаяЕдиница", "path": "1.3", "data_path_attribute_id": "1", "data_path_field_id": "0", "parameters": [], "semantic": {"groups": {}}},
    ]

    enrich_item_data_paths(items, attributes)

    assert items[0]["path_to_data"] == "Запись.Period"
    assert items[1]["path_to_data"] == "Запись.СтруктурнаяЕдиница"


def test_enrich_form_style_references_resolves_public_style_name(monkeypatch: pytest.MonkeyPatch) -> None:
    guid = "da164c9a-4836-4c07-ace9-1c2e2382eceb"
    prop = {"name": "BackColor", "value": {"kind": "StyleItem", "guid": guid, "status": "identity_pending"}}
    profile = {"items": [{"semantic": {"groups": {"Оформление": [prop]}}}]}
    monkeypatch.setattr(
        adapter_server,
        "metadata_cache_lookup_guid",
        lambda base_id, wanted_guid: {"kind": "StyleItem", "name": "ФонУправляющегоПоля", "guid": wanted_guid, "match_by": "cache"},
    )

    adapter_server.enrich_form_style_references("upo_test", profile)

    assert prop["value"] == "style:ФонУправляющегоПоля"
    assert prop["style_reference"] == {
        "kind": "StyleItem",
        "guid": guid,
        "name": "ФонУправляющегоПоля",
        "ref": "StyleItem.ФонУправляющегоПоля",
        "status": "ok",
        "resolved_by": "cache",
    }


def test_enrich_form_choice_list_references_resolves_public_enum_value(monkeypatch: pytest.MonkeyPatch) -> None:
    type_guid = "e36f8d8f-9d89-433b-bf81-6511e4141522"
    value_guid = "2d31cb6b-c0f4-4554-b5f0-b7343aa22a99"
    owner_guid = "4cddc788-29df-4050-8d5e-7c0af0587d3a"
    item = {
        "presentation": "",
        "value": {
            "kind": "EnumValue",
            "type_guid": type_guid,
            "value_guid": value_guid,
            "status": "identity_pending",
        },
    }
    profile = {
        "items": [
            {
                "semantic": {
                    "groups": {
                        "Использование": [
                            {"name": "ChoiceList", "value": {"count": 1, "items": [item]}}
                        ]
                    }
                }
            }
        ]
    }
    monkeypatch.setattr(
        adapter_server,
        "resolve_type_guids",
        lambda *_args, **_kwargs: {
            type_guid: {"kind": "Enum", "name": "Варианты", "owner_guid": owner_guid, "status": "ok"}
        },
    )
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *_args, **_kwargs: (b"enum", {}, None))
    monkeypatch.setattr(
        adapter_server,
        "decode_config_object_full",
        lambda *_args, **_kwargs: {
            "semantic": {
                "sections": [
                    {
                        "category": "EnumValue",
                        "records": [
                            {
                                "identity": {
                                    "guid": value_guid,
                                    "name": "Сложение",
                                    "synonyms": {"ru": "Сложение"},
                                }
                            }
                        ],
                    }
                ]
            }
        },
    )

    adapter_server.enrich_form_choice_list_references("upo_test", profile)

    assert item == {
        "presentation": "Сложение",
        "value": {
            "kind": "EnumValue",
            "ref": "Enum.Варианты.EnumValue.Сложение",
            "name": "Сложение",
            "status": "ok",
        },
    }


def test_enrich_form_command_references_adds_public_common_command(monkeypatch: pytest.MonkeyPatch) -> None:
    guid = "64a59186-ea83-44d7-b433-bf93778db6f9"
    profile = {"items": [{"name": "Кнопка", "command_reference": {"guid": guid}, "semantic": {"groups": {}}}]}
    monkeypatch.setattr(
        adapter_server,
        "metadata_cache_lookup_guid",
        lambda base_id, wanted_guid: {"kind": "CommonCommand", "name": "СвязанныеДокументы", "guid": wanted_guid},
    )

    adapter_server.enrich_form_command_references("upo_test", profile)

    row = profile["items"][0]
    assert row["command_name"] == "CommonCommand.СвязанныеДокументы"
    assert row["command_reference"]["status"] == "ok"
    assert row["semantic"]["groups"]["Основные"][-1]["name"] == "ИмяКоманды"


def test_enrich_form_command_references_reads_direct_sql_guid_when_cache_is_empty(monkeypatch: pytest.MonkeyPatch) -> None:
    guid = "64a59186-ea83-44d7-b433-bf93778db6f9"
    profile = {"items": [{"name": "Кнопка", "command_reference": {"guid": guid}, "semantic": {"groups": {}}}]}
    seen: dict[str, Any] = {}
    monkeypatch.setattr(adapter_server, "metadata_cache_lookup_guid", lambda base_id, wanted_guid: None)

    def fake_read(base_id: str, table: str, file_name: str, **kwargs: Any) -> tuple[bytes, dict[str, Any], None]:
        seen.update({"base_id": base_id, "table": table, "file_name": file_name})
        return b"command", {}, None

    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read)
    monkeypatch.setattr(
        adapter_server,
        "config_identity_from_bytes",
        lambda data: {"guid": guid, "name": "СвязанныеДокументы", "synonyms": {"ru": "Связанные документы"}},
    )

    adapter_server.enrich_form_command_references("upo_test", profile)

    assert seen == {"base_id": "upo_test", "table": "Config", "file_name": guid}
    assert profile["items"][0]["command_name"] == "CommonCommand.СвязанныеДокументы"


def test_form_public_sections_promotes_command_action_from_semantic() -> None:
    sections = adapter_server.form_public_sections(
        {
            "commands": [
                {
                    "name": "ЗаменаДомена",
                    "semantic": {
                        "groups": {
                            "Основные": [
                                {"name": "Action", "value": "ЗаменаДомена"},
                            ]
                        }
                    },
                }
            ],
            "command_links": [],
        }
    )

    assert sections["commands"][0]["action"] == "ЗаменаДомена"
    assert sections["commands"][0]["handler"] == "ЗаменаДомена"


def test_public_metadata_item_exposes_extension_origin_from_dbnames_route() -> None:
    item = public_metadata_item(
        {
            "likely_name": "фс_ЗдоровоеПитания",
            "type": {"kind": "boolean", "presentation": "Булево"},
            "storage_routes": [
                {
                    "source": "DBNames-Ext-f96a0c45-3eff-11f1-8287-005056b0d483",
                    "storage_role": "Fld",
                }
            ],
        },
        {},
        extensions_by_guid={
            "f96a0c45-3eff-11f1-8287-005056b0d483": {
                "name": "фс_ДоработкиОбщее",
                "guid": "f96a0c45-3eff-11f1-8287-005056b0d483",
                "active": True,
            }
        },
    )

    assert item["origin"]["source"] == "extension"
    assert item["origin"]["extension"]["name"] == "фс_ДоработкиОбщее"
    assert "storage_routes" not in item


def test_definition_find_allows_extension_only_without_object_selector() -> None:
    result = adapter_server.validate_metadata_definition_find_payload(
        {"base_id": "upo_test", "query": "фс_ДатаМобайлСписокДляВыгрузки", "areas": ["extensions"]}
    )

    assert result is None


def test_definition_find_allows_metadata_without_object_selector() -> None:
    result = adapter_server.validate_metadata_definition_find_payload(
        {"base_id": "upo_test", "query": "АвансовыйОтчет", "areas": ["metadata"]}
    )

    assert result is None

    result = adapter_server.validate_metadata_definition_find_payload(
        {
            "base_id": "upo_test",
            "query": "Реквизит",
            "areas": ["object"],
            "object_type": "Обработка",
            "object_name": "ТестоваяОбработка",
        }
    )

    assert result is None

    result = adapter_server.validate_metadata_definition_find_payload(
        {"base_id": "upo_test", "query": "ФормаДокумента", "areas": ["form"]}
    )

    assert result is not None
    assert result["status"] == "invalid_argument"
    assert result["argument"] == "name"


def test_configuration_definition_matches_from_metadata_cache(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
    config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    adapter_server.metadata_cache_upsert(
        config,
        {
            "kind": "Document",
            "kind_ru": "Документ",
            "public_kind": "document",
            "name": "АвансовыйОтчет",
            "synonym": "Авансовый отчет",
            "guid": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec",
            "source": "base",
        },
    )

    matches, diagnostics = adapter_server.metadata_configuration_definition_matches(
        base_id="upo_test",
        query="Авансовый отчет",
        max_matches=5,
        use_cache=True,
    )

    assert diagnostics == [{"area": "metadata", "source": "metadata_cache", "cache": "hit", "matches": 1}]
    assert len(matches) == 1
    assert matches[0]["area"] == "metadata"
    assert matches[0]["kind"] == "Документ"
    assert matches[0]["match_by"] == "synonym_exact"
    assert matches[0]["origin"]["source"] == "configuration"
    assert matches[0]["read_selector"]["method"] == "metadata.object.get"


def test_extension_definition_matches_resolve_owner_extension(monkeypatch: pytest.MonkeyPatch) -> None:
    class Record:
        guid = "d7c766c3-3ad6-41f5-92c0-c871981ad269"
        source = "DBNames-Ext-f96a0c45-3eff-11f1-8287-005056b0d483"
        storage_role = "InfoRg"
        sql_number = 62970

    payload_text = '{1,{3,{1,0,d7c766c3-3ad6-41f5-92c0-c871981ad269},"фс_ДатаМобайлСписокДляВыгрузки",{1,"ru","DataMobile Список для выгрузки"}}}'
    payload = payload_text.encode("utf-8")
    monkeypatch.setattr(adapter_server, "live_dbnames_records", lambda *args, **kwargs: ([Record()], None))
    monkeypatch.setattr(
        adapter_server,
        "extension_map_by_guid",
        lambda base_id: {
            "f96a0c45-3eff-11f1-8287-005056b0d483": {
                "name": "фс_ДоработкиОбщее",
                "guid": "f96a0c45-3eff-11f1-8287-005056b0d483",
                "active": True,
            }
        },
    )
    monkeypatch.setattr(adapter_server, "storage_files_list", lambda payload: {"status": "ok", "files": [{"FileName": "cas"}]})
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (payload, {}, None))
    monkeypatch.setattr(adapter_server, "read_storage_files_bytes", lambda *args, **kwargs: ({"cas": payload}, {}, None))
    monkeypatch.setattr(
        adapter_server,
        "payload_text_from_bytes",
        lambda data: {"text": payload_text},
    )
    monkeypatch.setattr(
        adapter_server,
        "config_identity_from_bytes",
        lambda data: {
            "guid": "d7c766c3-3ad6-41f5-92c0-c871981ad269",
            "name": "фс_ДатаМобайлСписокДляВыгрузки",
            "synonyms": {"ru": "DataMobile Список для выгрузки"},
        },
    )

    matches, diagnostics = adapter_server.metadata_extension_definition_matches(
        base_id="upo_test",
        query="фс_ДатаМобайлСписокДляВыгрузки",
        max_files=10,
        max_matches=5,
        timeout_seconds=30,
    )

    assert diagnostics[-1]["indexed_extension_guids"] == 1
    assert len(matches) == 1
    assert matches[0]["origin"]["extension"]["name"] == "фс_ДоработкиОбщее"
    assert matches[0]["kind"] == "РегистрСведений"


def test_extension_definition_cache_lookup_returns_public_match(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
    config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))

    adapter_server.metadata_extension_definition_cache_upsert(
        config,
        base_id="upo_test",
        identity={
            "guid": "d7c766c3-3ad6-41f5-92c0-c871981ad269",
            "name": "фс_ДатаМобайлСписокДляВыгрузки",
            "synonyms": {"ru": "DataMobile Список для выгрузки"},
        },
        source_item={
            "extension": {
                "guid": "f96a0c45-3eff-11f1-8287-005056b0d483",
                "name": "фс_ДоработкиОбщее",
                "active": True,
            },
            "storage_role": "InfoRg",
        },
        file_name="cas",
    )

    matches, marker = adapter_server.metadata_extension_definition_cache_lookup(
        "upo_test",
        query="фс_ДатаМобайлСписокДляВыгрузки",
        max_matches=5,
    )

    assert marker is None
    assert len(matches) == 1
    assert matches[0]["match_by"] == "name_exact"
    assert matches[0]["origin"]["source"] == "extension"
    assert matches[0]["origin"]["extension"]["name"] == "фс_ДоработкиОбщее"
    assert matches[0]["location"]["presentation"] == "Расширение.фс_ДоработкиОбщее.РегистрСведений.фс_ДатаМобайлСписокДляВыгрузки"

    contains_matches, _ = adapter_server.metadata_extension_definition_cache_lookup(
        "upo_test",
        query="ДатаМобайл",
        max_matches=5,
    )
    assert len(contains_matches) == 1
    assert contains_matches[0]["match_by"] == "name_contains"


def test_extension_definition_cache_complete_marker_returns_fast_not_found(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
    config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))

    adapter_server.metadata_guid_index_upsert(
        config,
        {
            "guid": adapter_server.EXTENSION_DEFINITION_CACHE_MARKER_GUID,
            "guid_role": adapter_server.EXTENSION_DEFINITION_CACHE_MARKER_ROLE,
            "kind": "ExtensionDefinitionCache",
            "kind_ru": "Индекс определений расширений",
            "name": "Индекс определений расширений",
            "presentation": "Индекс определений расширений",
            "payload": {"status": "complete", "indexed_definitions": 1},
        },
    )

    def fail_live_scan(*args, **kwargs):
        raise AssertionError("live scan must not run when complete cache marker exists")

    monkeypatch.setattr(adapter_server, "live_dbnames_records", fail_live_scan)

    matches, diagnostics = adapter_server.metadata_extension_definition_matches(
        base_id="upo_test",
        query="НетТакогоИмени",
        max_files=10,
        max_matches=5,
        timeout_seconds=30,
        use_cache=True,
    )

    assert matches == []
    assert diagnostics == [{"area": "extensions", "cache": "hit_empty", "indexed_definitions": 1}]


def test_payload_decoder_prefers_cyrillic_cp1251_over_false_utf16() -> None:
    text = '{1,"\u0444\u0441_\u041f\u0424_MXL_\u041c\u0435\u043d\u044e\u0417\u0430\u041f\u0435\u0440\u0438\u043e\u0434"}'

    decoded, encoding = try_decode(text.encode("cp1251"))

    assert decoded == text
    assert encoding == "cp1251"


def test_identity_matching_repairs_latin1_cp1251_mojibake() -> None:
    name = "\u0444\u0441_\u041f\u0424_MXL_\u041c\u0435\u043d\u044e\u0417\u0430\u041f\u0435\u0440\u0438\u043e\u0434"
    mojibake_name = name.encode("cp1251").decode("latin1")

    assert adapter_server.best_text_variant(mojibake_name) == name
    assert adapter_server.extension_definition_identity_match_by({"name": mojibake_name, "synonyms": {}}, name) == "name_exact"


def test_public_metadata_rows_repair_latin1_cp1251_names() -> None:
    name = "АвансовыйОтчетПрисоединенныеФайлы"
    mojibake_name = name.encode("cp1251").decode("latin1")

    live = adapter_server.public_metadata_row(
        {
            "guid": "11111111-2222-3333-4444-555555555555",
            "kind": "Catalog",
            "name": mojibake_name,
            "identity": {"name": mojibake_name, "synonyms": {"ru": mojibake_name}},
        }
    )
    cached = adapter_server.metadata_cache_public_row(
        {
            "guid": "11111111-2222-3333-4444-555555555555",
            "kind": "Catalog",
            "name": mojibake_name,
            "synonym": mojibake_name,
        }
    )

    assert live["name"] == name
    assert live["ref"] == f"Catalog.{name}"
    assert live["identity"]["name"] == name
    assert live["identity"]["synonyms"]["ru"] == name
    assert cached["name"] == name
    assert cached["synonym"] == name


def test_extension_objects_find_uses_manifest_descriptor_route(monkeypatch: pytest.MonkeyPatch) -> None:
    descriptor_key = "a" * 40
    part_key = "b" * 40
    object_base_id = "7c6dc711-02f5-4e6a-9f37-7dd57d8942b3"
    guid = "11111111-2222-3333-4444-555555555555"
    name = "\u0444\u0441_\u041f\u0424_MXL_\u041c\u0435\u043d\u044e\u0417\u0430\u041f\u0435\u0440\u0438\u043e\u0434"
    mojibake_name = name.encode("cp1251").decode("latin1")
    descriptor_payload = f'{{1,{{3,{{1,0,{guid}}},"{mojibake_name}",{{1,"ru","Test template"}}}}}}'.encode("latin1")

    monkeypatch.setattr(adapter_server, "extension_definition_guid_sources", lambda *args, **kwargs: ({}, None))
    monkeypatch.setattr(
        adapter_server,
        "live_extension_manifests",
        lambda *args, **kwargs: (
            [
                {
                    "status": "ok",
                    "root_cas_key": "c" * 40,
                    "extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"},
                    "entries": [
                        {"object_id": object_base_id, "object_base_id": object_base_id, "suffix": "", "cas_key": descriptor_key},
                        {"object_id": f"{object_base_id}.0", "object_base_id": object_base_id, "suffix": ".0", "cas_key": part_key},
                    ],
                }
            ],
            [],
        ),
    )
    monkeypatch.setattr(adapter_server, "read_storage_files_bytes", lambda *args, **kwargs: ({descriptor_key: descriptor_payload}, {}, None))

    result = adapter_server.extension_objects_find(
        {
            "base_id": "upo_test",
            "kind": "Template",
            "query": name,
            "include_storage": True,
            "limit": 1,
        }
    )

    assert result["status"] == "ok"
    assert result["objects"][0]["name"] == name
    assert result["objects"][0]["kind"] == "Template"
    assert result["objects"][0]["route"]["route_type"] == "extension_manifest_cas"
    assert result["objects"][0]["route"]["file_name"] == descriptor_key
    assert result["objects"][0]["route"]["manifest_entries"] == 2
    assert result["counts"]["manifest_matches"] == 1


def test_extension_objects_find_uses_validated_source_cache(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
    config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
    descriptor_key = "a" * 40
    part_key = "b" * 40
    object_base_id = "7c6dc711-02f5-4e6a-9f37-7dd57d8942b3"
    name = "\u0444\u0441_\u041f\u0424_MXL_\u041c\u0435\u043d\u044e\u0417\u0430\u041f\u0435\u0440\u0438\u043e\u0434"

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    monkeypatch.setattr(adapter_server, "extension_definition_guid_sources", lambda *args, **kwargs: ({}, None))
    monkeypatch.setattr(
        adapter_server,
        "live_extension_manifests",
        lambda *args, **kwargs: (
            [
                {
                    "root_cas_key": "c" * 40,
                    "extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"},
                    "entries": [
                        {"object_id": object_base_id, "object_base_id": object_base_id, "suffix": "", "cas_key": descriptor_key},
                        {"object_id": f"{object_base_id}.0", "object_base_id": object_base_id, "suffix": ".0", "cas_key": part_key},
                    ],
                }
            ],
            [],
        ),
    )

    route = {
        "route_type": "extension_manifest_cas",
        "table": "ConfigCAS",
        "file_name": descriptor_key,
        "manifest_entries": 2,
        "manifest_entry": {
            "object_id": object_base_id,
            "object_base_id": object_base_id,
            "suffix": "",
            "cas_key": descriptor_key,
            "extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"},
            "root_cas_key": "c" * 40,
        },
    }
    adapter_server.extension_route_cache_upsert(
        config,
        {
            "kind": "Template",
            "kind_ru": "Макет",
            "name": name,
            "guid": descriptor_key,
            "origin": {"extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"}},
            "route": route,
            "manifest_entries": [],
        },
        descriptor_payload_sha1="sha",
    )

    result = adapter_server.extension_objects_find({"base_id": "upo_test", "kind": "Template", "query": name, "limit": 1})

    assert result["status"] == "ok"
    assert result["objects"][0]["match_by"] == "source_cache"
    assert result["objects"][0]["freshness"]["status"] == "fresh"
    assert result["counts"]["source_cache_validated"] == 1
    assert result["counts"]["manifest_matches"] == 0


def test_extension_objects_find_skips_deep_scan_for_extension_by_default(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "99999999-9999-9999-9999-999999999999"
    descriptor_key = "a" * 40
    match = {
        "kind": "CommonForm",
        "kind_ru": "ОбщаяФорма",
        "name": "TestForm",
        "guid": "11111111-2222-3333-4444-555555555555",
        "match_by": "manifest_descriptor",
        "origin": {"source": "extension", "extension": {"name": "test2", "guid": extension_guid}, "status": "ok"},
        "route": {"route_type": "extension_manifest_cas", "table": "ConfigCAS", "file_name": descriptor_key},
        "read_selectors": {},
    }

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"database": base_id}, None))
    monkeypatch.setattr(adapter_server, "extension_filter_to_guid", lambda base_id, extension, method: (extension_guid, None))
    monkeypatch.setattr(adapter_server, "extension_route_cache_lookup", lambda *args, **kwargs: [])
    monkeypatch.setattr(
        adapter_server,
        "extension_manifest_object_matches",
        lambda *args, **kwargs: ([match], [], {"manifest_count": 1, "descriptor_entries": 1, "descriptor_payloads_read": 1}),
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_forms_search",
        lambda *args, **kwargs: {"status": "not_found", "forms": [], "counts": {"scanned": 0}},
    )

    def fail_storage_scan(payload: dict[str, Any]) -> dict[str, Any]:
        raise AssertionError("ConfigCAS scan must be explicit for extension-scoped lookups")

    monkeypatch.setattr(adapter_server, "storage_files_list", fail_storage_scan)

    result = adapter_server.extension_objects_find({"base_id": "upo_test", "extension": "test2", "limit": 50})

    assert result["status"] == "ok"
    assert result["objects"] == [match]
    assert result["counts"]["scanned_payloads"] == 0
    assert result["counts"]["configcas_scan_skipped"] is True
    assert result["query"]["full_scan"] is False
    assert result["diagnostics"][0]["reason"] == "full_scan_disabled_for_extension"


def test_extension_objects_find_defaults_to_working_saved_state(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    saved_guid = "caee4437-14b6-436f-98e3-360c4577f20d"
    saved_file = f"{extension_guid}__{saved_guid}.0"

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"database": base_id}, None))
    monkeypatch.setattr(adapter_server, "extension_filter_to_guid", lambda base_id, extension, method: (extension_guid, None))
    monkeypatch.setattr(adapter_server, "extension_route_cache_lookup", lambda *args, **kwargs: [])
    monkeypatch.setattr(
        adapter_server,
        "extension_manifest_object_matches",
        lambda *args, **kwargs: ([], [], {"manifest_count": 0, "descriptor_entries": 0, "descriptor_payloads_read": 0}),
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_forms_search",
        lambda *args, **kwargs: {
            "status": "ok",
            "forms": [
                {
                    "table": "ConfigCASSave",
                    "file_name": saved_file,
                    "name": "tt_Форма6",
                    "form": {
                        "identity": {
                            "source": "saved_state_descriptor",
                            "name": "tt_Форма6",
                            "synonym": "Форма 6",
                            "guid": saved_guid,
                            "descriptor_file_name": saved_file[:-2],
                        }
                    },
                }
            ],
            "counts": {"scanned": 2},
        },
    )

    result = adapter_server.extension_objects_find({"base_id": "upo_test", "extension": "test2", "limit": 50})

    assert result["status"] == "ok"
    assert result["query"]["state"] == "working"
    assert result["objects"][0]["name"] == "tt_Форма6"
    assert result["objects"][0]["activation_state"] == "saved_only"
    assert result["objects"][0]["route"]["table"] == "ConfigCASSave"
    assert result["objects"][0]["read_selectors"]["form_decode"]["table"] == "ConfigCASSave"
    assert result["counts"]["saved_state_matches"] == 1
    assert result["counts"]["activation_state"] == {"saved_only": 1}


def test_extension_objects_find_decodes_saved_calculation_register(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
    file_name = f"{extension_guid}__{object_guid}"
    descriptor = '{1,{21,"tt_CalcRegister1"},0,0,0,0,0,0,0,0}'.encode("utf-8")

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"database": base_id}, None))
    monkeypatch.setattr(adapter_server, "extension_filter_to_guid", lambda base_id, extension, method: (extension_guid, None))
    monkeypatch.setattr(adapter_server, "extension_route_cache_lookup", lambda *args, **kwargs: [])
    monkeypatch.setattr(
        adapter_server,
        "extension_manifest_object_matches",
        lambda *args, **kwargs: ([], [], {"manifest_count": 0, "descriptor_entries": 0, "descriptor_payloads_read": 0}),
    )
    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {"status": "ok", "files": [{"FileName": file_name}]},
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_files_bytes",
        lambda *args, **kwargs: ({file_name: descriptor}, {"database": "upo_test"}, None),
    )

    result = adapter_server.extension_objects_find(
        {
            "base_id": "upo_test",
            "extension": "test2",
            "kind": "CalculationRegister",
            "query": "tt_CalcRegister1",
            "limit": 20,
        }
    )

    assert result["status"] == "ok"
    assert result["objects"][0]["kind"] == "CalculationRegister"
    assert result["objects"][0]["name"] == "tt_CalcRegister1"
    assert result["objects"][0]["guid"] == object_guid
    assert result["objects"][0]["activation_state"] == "saved_only"
    assert result["objects"][0]["route"]["route_type"] == "saved_state_metadata"
    assert result["objects"][0]["route"]["table"] == "ConfigCASSave"
    assert result["objects"][0]["read_selectors"]["card"]["file_name"] == file_name
    assert result["objects"][0]["read_selectors"]["card"]["extension_guid"] == extension_guid
    assert result["counts"]["saved_state_scanned"] == 1


def test_metadata_object_get_reconstructs_saved_descriptor_file_name(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
    file_name = f"{extension_guid}__{object_guid}"
    descriptor = '{1,{21,"tt_CalcRegister1"},0,0,0,0,0,0,0,0}'.encode("utf-8")
    read_calls: list[tuple[str, str, str]] = []

    def fake_read(base_id: str, table: str, requested_file_name: str, **kwargs: Any) -> tuple[bytes, dict[str, str], None]:
        read_calls.append((base_id, table, requested_file_name))
        return descriptor, {"database": "upo_test"}, None

    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read)

    result = adapter_server.get_object(
        "CalculationRegister",
        object_guid,
        base_id="upo_test",
        table="ConfigCASSave",
        extension_guid=extension_guid,
        include_semantic=False,
    )

    assert result["status"] == "ok"
    assert result["source"] == {"kind": "live_sql", "table": "ConfigCASSave"}
    assert result["object"]["kind"] == "CalculationRegister"
    assert result["object"]["name"] == "tt_CalcRegister1"
    assert result["object"]["guid"] == object_guid
    assert result["counts"]["scanned"] == 1
    assert read_calls == [("upo_test", "ConfigCASSave", file_name)]


def test_metadata_object_get_dispatch_prefers_guid_for_saved_extension_selector(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
    captured: dict[str, Any] = {}

    def fake_get_object(kind: str | None, name: str, **kwargs: Any) -> dict[str, Any]:
        captured.update({"kind": kind, "name": name, **kwargs})
        return {"status": "ok"}

    monkeypatch.setattr(adapter_server, "get_object", fake_get_object)

    result = adapter_server.call_method_impl(
        "metadata.object.get",
        {
            "base_id": "upo_test",
            "kind": "CalculationRegister",
            "guid": object_guid,
            "name": "tt_CalcRegister1",
            "table": "ConfigCASSave",
            "extension_guid": extension_guid,
        },
    )

    assert result["status"] == "ok"
    assert captured["name"] == object_guid
    assert captured["extension_guid"] == extension_guid
    assert captured["table"] == "ConfigCASSave"


def test_metadata_object_properties_reads_saved_calculation_register_selector(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
    chart_guid = "b69453f1-42f1-449c-b051-185324b13824"
    file_name = f"{extension_guid}__{object_guid}"
    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    header = [atom(21)]
    while len(header) <= 31:
        header.append(atom(0))
    header[16] = atom(2)
    header[17] = atom(0)
    header[18] = atom(0)
    header[22] = atom(chart_guid)
    header[24] = atom(1)
    header[25] = atom(0)
    header[26] = atom(1)
    header[27] = atom(0)
    tree = seq(atom(1), seq(*header))
    read_calls: list[tuple[str, str, str]] = []

    def fake_read(base_id: str, table: str, requested_file_name: str, **kwargs: Any) -> tuple[bytes, dict[str, str], None]:
        read_calls.append((base_id, table, requested_file_name))
        return (b"chart" if requested_file_name.endswith(chart_guid) else b"descriptor"), {"database": "upo_test"}, None

    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read)
    monkeypatch.setattr(adapter_server, "parse_config_tree_from_bytes", lambda data: tree)
    monkeypatch.setattr(
        adapter_server,
        "config_identity_from_bytes",
        lambda data: {"guid": chart_guid, "name": "tt_Chart1"} if data == b"chart" else {"guid": object_guid, "name": "tt_CalcRegister1"},
    )

    result = adapter_server.metadata_object_properties(
        {
            "base_id": "upo_test",
            "kind": "CalculationRegister",
            "guid": object_guid,
            "name": "tt_CalcRegister1",
            "table": "ConfigCASSave",
            "extension_guid": extension_guid,
        }
    )

    assert result["status"] == "ok"
    assert result["decoder"] == "kind_specific_sql"
    assert result["object"]["guid"] == object_guid
    assert result["properties"]["periodicity"] == "Month"
    assert result["properties"]["chart_of_calculation_types"]["guid"] == chart_guid
    assert result["properties"]["chart_of_calculation_types"]["name"] == "tt_Chart1"
    assert result["properties"]["chart_of_calculation_types"]["status"] == "ok"
    assert read_calls == [
        ("upo_test", "ConfigCASSave", file_name),
        ("upo_test", "ConfigCASSave", f"{extension_guid}__{chart_guid}"),
    ]


def test_metadata_object_attributes_reads_saved_extension_register_fields(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
    calls: list[dict[str, Any]] = []
    object_card = {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid}

    def fake_get_object(kind: str | None, name: str, **kwargs: Any) -> dict[str, Any]:
        calls.append({"kind": kind, "name": name, **kwargs})
        result: dict[str, Any] = {"status": "ok", "object": object_card}
        if kwargs.get("include_semantic", True):
            result["semantic"] = {
                "sections": [
                    {"category": "Attribute", "records": [{"likely_name": "Реквизит1", "type": {"kind": "string", "presentation": "Строка(10)"}}]},
                    {"category": "Resource", "records": [{"likely_name": "Ресурс1", "type": {"kind": "number", "presentation": "Число(10, 0)"}}]},
                    {"category": "Dimension", "records": [{"likely_name": "Измерение1", "type": {"kind": "string", "presentation": "Строка(10)"}}]},
                ]
            }
        return result

    monkeypatch.setattr(adapter_server, "get_object", fake_get_object)
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
    monkeypatch.setattr(adapter_server, "extension_map_by_guid", lambda base_id: {})

    result = adapter_server.metadata_object_attributes(
        {
            "base_id": "upo_test",
            "kind": "CalculationRegister",
            "guid": object_guid,
            "table": "ConfigCASSave",
            "extension_guid": extension_guid,
            "only": "register_fields",
        }
    )

    assert result["status"] == "ok"
    assert [item["name"] for item in result["attributes"]] == ["Реквизит1"]
    assert [item["name"] for item in result["resources"]] == ["Ресурс1"]
    assert [item["name"] for item in result["dimensions"]] == ["Измерение1"]
    assert result["query"]["extension_guid"] == extension_guid
    assert len(calls) == 2
    assert all(call["extension_guid"] == extension_guid for call in calls)
    assert calls[0]["table"] == "ConfigCASSave"


def test_metadata_object_templates_includes_decoded_data_composition_schema(monkeypatch: pytest.MonkeyPatch) -> None:
    schema_guid = "11111111-1111-1111-1111-111111111111"
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_related",
        lambda payload: {
            "status": "ok",
            "object": {"kind": "Report", "name": "Продажи"},
            "related": [{
                "category": "DataCompositionSchema", "status": "ok", "guid": schema_guid,
                "identity": {"guid": schema_guid, "name": "ОсновнаяСхемаКомпоновкиДанных", "synonyms": {"ru": "Основная схема компоновки данных"}},
            }],
            "counts": {"related": 1},
        },
    )
    monkeypatch.setattr(adapter_server, "metadata_object_parts", lambda payload: {"status": "ok", "parts": []})

    result = adapter_server.metadata_object_templates(
        {"base_id": "upo_test", "ref": "Отчет.Продажи", "template": "ОсновнаяСхемаКомпоновкиДанных"}
    )

    assert result["status"] == "ok"
    assert result["counts"]["templates"] == 1
    assert result["templates"][0]["name"] == "ОсновнаяСхемаКомпоновкиДанных"
    assert result["templates"][0]["kind"] == "data_composition_schema"
    assert result["templates"][0]["match_by"] == "name_exact"
    assert result["templates"][0]["platform_type"] == "Схема компоновки данных"
    assert result["templates"][0]["template_type_candidates"] == [{
        **adapter_server.onec_template_type_by_id("data_composition_schema"),
        "confidence": "high",
        "evidence": ["confirmed Template identity"],
    }]


def test_templates_bindings_preserves_data_composition_schema_kind(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_templates",
        lambda payload: {
            "status": "ok", "object": {"kind": "Report", "name": "Продажи"}, "source": {"kind": "live_metadata"},
            "templates": [{"name": "ОсновнаяСхемаКомпоновкиДанных", "kind": "data_composition_schema", "format": "НеОпределено"}],
            "counts": {"templates": 1},
        },
    )
    monkeypatch.setattr(adapter_server, "metadata_code_index_search", lambda payload: {"status": "not_found", "matches": []})
    monkeypatch.setattr(adapter_server, "search_modules", lambda payload: {"status": "not_found", "matches": []})

    result = adapter_server.templates_bindings(
        {"base_id": "upo_test", "ref": "Отчет.Продажи", "template": "ОсновнаяСхемаКомпоновкиДанных"}
    )

    assert result["status"] == "ok"
    assert result["bindings"][0]["template"]["kind"] == "data_composition_schema"
    assert result["bindings"][0]["format"] == "data_composition_schema"


def test_template_field_lineage_routes_selected_scd_directly(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "templates_bindings",
        lambda payload: {
            "status": "ok", "object": {"kind": "Report", "name": "Продажи"},
            "bindings": [{"template": {"name": "ОсновнаяСхемаКомпоновкиДанных", "kind": "data_composition_schema"}}],
        },
    )
    seen_payload: dict[str, Any] = {}

    def fake_dcs_field_lineage(payload: dict[str, Any]) -> dict[str, Any]:
        seen_payload.update(payload)
        return {
            "status": "ok", "lineage": [{"field": "Цена", "expression": "Цены.Цена"}],
            "agent_summary": {"confirmed_sources": [{"source": "РегистрСведений.Цены.Цена"}]},
        }

    monkeypatch.setattr(adapter_server, "dcs_field_lineage", fake_dcs_field_lineage)

    result = adapter_server.template_field_lineage(
        {"base_id": "upo_test", "ref": "Отчет.Продажи", "template": "ОсновнаяСхемаКомпоновкиДанных", "field": "Цена", "resolve_scd": True}
    )

    assert result["status"] == "confirmed"
    assert result["lineage"][0]["resolution"] == "direct_scd_field_lineage"
    assert result["source_context_candidates"][0]["confirmed_sources"] == [{"source": "РегистрСведений.Цены.Цена"}]
    assert seen_payload["schema"] == "ОсновнаяСхемаКомпоновкиДанных"
    assert seen_payload["report"] == "Продажи"


def test_metadata_object_related_reads_saved_extension_child_with_internal_prefix(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
    child_guid = "10000000-0000-0000-0000-000000000004"
    object_file_name = f"{extension_guid}__{object_guid}"
    child_file_name = f"{extension_guid}__{child_guid}"
    atom = lambda value: {"type": "atom", "value": str(value)}
    string = lambda value: {"type": "string", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    identity = seq(seq(atom(1), atom(0), atom(child_guid)), string("Перерасчет1"), seq(atom(0)))
    root_items = [atom(1), *(seq(atom(f"marker-{index}"), atom(0)) for index in range(1, 10))]
    root_items[4] = seq(atom("recalculation-marker"), atom(1), identity)
    tree = seq(*root_items)
    get_calls: list[dict[str, Any]] = []
    read_calls: list[str] = []

    def fake_get_object(kind: str | None, name: str, **kwargs: Any) -> dict[str, Any]:
        get_calls.append({"kind": kind, "name": name, **kwargs})
        return {
            "status": "ok",
            "object": {
                "kind": "CalculationRegister",
                "name": "tt_РегистрРасчета1",
                "guid": object_guid,
                "storage": {"table": "ConfigCASSave", "file_name": object_file_name},
            },
        }

    def fake_read(base_id: str, table: str, file_name: str, **kwargs: Any) -> tuple[bytes, dict[str, str], None]:
        read_calls.append(file_name)
        return (b'{1}' if file_name == child_file_name else b'{0}'), {"database": "upo_test"}, None

    monkeypatch.setattr(adapter_server, "get_object", fake_get_object)
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read)
    monkeypatch.setattr(adapter_server, "parse_config_tree_from_bytes", lambda data: tree)
    monkeypatch.setattr(adapter_server, "config_identity_from_bytes", lambda data: {"guid": child_guid, "name": "Перерасчет1"})

    result = adapter_server.metadata_object_related(
        {
            "base_id": "upo_test",
            "kind": "CalculationRegister",
            "guid": object_guid,
            "table": "ConfigCASSave",
            "extension_guid": extension_guid,
        }
    )

    assert result["status"] == "ok"
    assert result["related"][0]["category"] == "Recalculation"
    assert result["related"][0]["identity"]["name"] == "Перерасчет1"
    assert get_calls[0]["extension_guid"] == extension_guid
    assert read_calls == [object_file_name, child_file_name]


def test_metadata_object_related_saved_command_uses_identity_and_module_suffix(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
    command_guid = "a9b2fc38-86b6-4586-8aca-29b92bdd86de"
    command_class_guid = "078a6af8-d22c-4248-9c33-7e90075a3d2c"
    object_file_name = f"{extension_guid}__{object_guid}"
    command_file_name = f"{extension_guid}__{command_guid}"
    atom = lambda value: {"type": "atom", "value": str(value)}
    string = lambda value: {"type": "string", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    identity = seq(seq(atom(1), atom(0), atom(command_guid)), string("Команда1"), seq(atom(0)))
    command_record = seq(identity, atom(command_class_guid))
    root_items = [atom(1), *(seq(atom(f"marker-{index}"), atom(0)) for index in range(1, 10))]
    root_items[8] = seq(atom("command-marker"), atom(1), command_record)
    tree = seq(*root_items)
    read_calls: list[str] = []

    monkeypatch.setattr(
        adapter_server,
        "get_object",
        lambda *args, **kwargs: {
            "status": "ok",
            "object": {
                "kind": "CalculationRegister",
                "name": "tt_РегистрРасчета1",
                "guid": object_guid,
                "storage": {"table": "ConfigCASSave", "file_name": object_file_name},
            },
        },
    )

    def fake_read(base_id: str, table: str, file_name: str, **kwargs: Any) -> tuple[Any, Any, Any]:
        read_calls.append(file_name)
        if file_name == object_file_name:
            return b"object", {"database": "upo_test"}, None
        if file_name == f"{command_file_name}.2":
            return b'{1,"module"}', {"database": "upo_test"}, None
        return None, {"database": "upo_test"}, {"status": "source_missing", "diagnostics": {"message": "missing"}}

    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read)
    monkeypatch.setattr(adapter_server, "parse_config_tree_from_bytes", lambda data: tree)
    monkeypatch.setattr(adapter_server, "config_identity_from_bytes", lambda data: None)

    result = adapter_server.metadata_object_related(
        {
            "base_id": "upo_test",
            "kind": "CalculationRegister",
            "guid": object_guid,
            "table": "ConfigCASSave",
            "extension_guid": extension_guid,
        }
    )

    assert result["status"] == "ok"
    assert result["counts"]["related"] == 1
    assert result["related"][0]["category"] == "Command"
    assert result["related"][0]["guid"] == command_guid
    assert result["related"][0]["identity"]["name"] == "Команда1"
    assert command_class_guid not in [item.get("guid") for item in result["related"]]
    assert read_calls == [object_file_name, command_file_name, f"{command_file_name}.2"]


def test_metadata_object_forms_reads_saved_form_payload_route(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
    form_guid = "a329697c-32b1-4601-8121-6703516be515"
    form_file_name = f"{extension_guid}__{form_guid}.0"
    get_calls: list[dict[str, Any]] = []
    read_calls: list[str] = []

    def fake_get_object(kind: str | None, name: str, **kwargs: Any) -> dict[str, Any]:
        get_calls.append(kwargs)
        return {
            "status": "ok",
            "object": {
                "kind": "CalculationRegister",
                "name": "tt_РегистрРасчета1",
                "guid": object_guid,
                "storage": {"table": "ConfigCASSave", "file_name": f"{extension_guid}__{object_guid}"},
            },
        }

    monkeypatch.setattr(adapter_server, "get_object", fake_get_object)
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_related",
        lambda payload: {
            "status": "ok",
            "object": {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid},
            "source": {"kind": "live_metadata"},
            "related": [{"category": "Form", "guid": form_guid, "status": "ok", "identity": {"guid": form_guid, "name": "ФормаСписка", "synonyms": {}}}],
            "counts": {"related": 1, "by_category": {"Form": 1}},
        },
    )

    def fake_read(base_id: str, table: str, file_name: str, **kwargs: Any) -> tuple[bytes, dict[str, str], None]:
        read_calls.append(file_name)
        return b'{1,"form"}', {"database": "upo_test"}, None

    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read)

    result = adapter_server.metadata_object_forms(
        {
            "base_id": "upo_test",
            "kind": "CalculationRegister",
            "guid": object_guid,
            "table": "ConfigCASSave",
            "extension_guid": extension_guid,
            "include_storage": True,
        }
    )

    assert result["status"] == "ok"
    assert result["forms"][0]["name"] == "ФормаСписка"
    assert result["forms"][0]["source"]["file_name"] == form_file_name
    assert get_calls[0]["extension_guid"] == extension_guid
    assert read_calls == [form_file_name]


def test_metadata_object_commands_forwards_saved_extension_selector(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
    command_guid = "a9b2fc38-86b6-4586-8aca-29b92bdd86de"
    get_calls: list[dict[str, Any]] = []

    def fake_get_object(kind: str | None, name: str, **kwargs: Any) -> dict[str, Any]:
        get_calls.append(kwargs)
        return {"status": "ok", "object": {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid}}

    monkeypatch.setattr(adapter_server, "get_object", fake_get_object)
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_related",
        lambda payload: {
            "status": "ok",
            "related": [{"category": "Command", "guid": command_guid, "status": "ok", "identity": {"guid": command_guid, "name": "Команда1", "synonyms": {}}}],
            "counts": {"related": 1, "by_category": {"Command": 1}},
        },
    )

    result = adapter_server.metadata_object_commands(
        {
            "base_id": "upo_test",
            "kind": "CalculationRegister",
            "guid": object_guid,
            "table": "ConfigCASSave",
            "extension_guid": extension_guid,
            "include_form_commands": False,
        }
    )

    assert result["status"] == "ok"
    assert [item["name"] for item in result["object_commands"]] == ["Команда1"]
    assert result["object_commands"][0]["module"] == {"kind": "command_module", "name": "Модуль команды"}
    assert result["object_commands"][0]["read_selector"] == {
        "method": "modules.read",
        "base_id": "upo_test",
        "kind": "CalculationRegister",
        "guid": object_guid,
        "module_ref": f"ConfigCASSave:{extension_guid}__{command_guid}.2",
    }
    assert get_calls[0]["extension_guid"] == extension_guid


def test_metadata_object_commands_resolves_public_extension_before_object_probe(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
    get_calls: list[dict[str, Any]] = []

    monkeypatch.setattr(
        adapter_server,
        "extension_filter_to_guid",
        lambda base_id, extension, method: (extension_guid, None),
    )

    def fake_get_object(kind: str | None, name: str, **kwargs: Any) -> dict[str, Any]:
        get_calls.append(kwargs)
        return {"status": "ok", "object": {"kind": "Report", "name": "Отчет", "guid": object_guid}}

    monkeypatch.setattr(adapter_server, "get_object", fake_get_object)
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_related",
        lambda payload: {"status": "ok", "related": [], "counts": {"related": 0, "by_category": {}}},
    )

    result = adapter_server.metadata_object_commands(
        {
            "base_id": "upo_test",
            "kind": "Report",
            "name": "Отчет",
            "extension": "РасширениеОтчетов",
            "include_form_commands": False,
        }
    )

    assert result["status"] == "ok"
    assert get_calls[0]["extension_guid"] == extension_guid
    assert get_calls[0]["table"] == "ConfigCAS"


def test_metadata_object_commands_keeps_resolved_extension_for_child_decoder(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
    related_calls: list[dict[str, Any]] = []

    monkeypatch.setattr(
        adapter_server,
        "get_object",
        lambda *args, **kwargs: {"status": "ok", "object": {"kind": "Report", "name": "Отчет", "guid": object_guid}},
    )
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))

    def fake_related(payload: dict[str, Any]) -> dict[str, Any]:
        related_calls.append(payload)
        return {"status": "ok", "related": [], "counts": {"related": 0, "by_category": {}}}

    monkeypatch.setattr(adapter_server, "metadata_object_related", fake_related)

    result = adapter_server.metadata_object_commands(
        {
            "base_id": "upo_test",
            "kind": "Report",
            "guid": object_guid,
            "table": "ConfigCAS",
            "extension_guid": extension_guid,
            "include_form_commands": False,
        }
    )

    assert result["status"] == "ok"
    assert related_calls[0]["extension_guid"] == extension_guid


def test_metadata_object_modules_forwards_saved_extension_selector(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
    get_calls: list[dict[str, Any]] = []

    def fake_get_object(kind: str | None, name: str, **kwargs: Any) -> dict[str, Any]:
        get_calls.append(kwargs)
        return {"status": "ok", "object": {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid}}

    monkeypatch.setattr(adapter_server, "get_object", fake_get_object)
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_parts",
        lambda payload: {
            "status": "ok",
            "object": {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid},
            "source": {"kind": "live_metadata"},
            "parts": [],
            "counts": {"parts": 0, "roles": {}},
        },
    )

    result = adapter_server.metadata_object_modules(
        {
            "base_id": "upo_test",
            "kind": "CalculationRegister",
            "guid": object_guid,
            "table": "ConfigCASSave",
            "extension_guid": extension_guid,
        }
    )

    assert result["status"] == "ok"
    assert result["modules"] == []
    assert get_calls[0]["extension_guid"] == extension_guid


def test_metadata_object_modules_auto_resolves_exact_extension_object(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    object_guid = "e7e322f5-a7a6-4ddc-bad0-344e75ad00d0"
    get_calls: list[dict[str, Any]] = []

    def fake_get_object(kind: str | None, name: str, **kwargs: Any) -> dict[str, Any]:
        get_calls.append({"kind": kind, "name": name, **kwargs})
        return {"status": "ok", "object": {"kind": "Catalog", "name": "test2", "guid": object_guid}}

    monkeypatch.setattr(adapter_server, "get_object", fake_get_object)
    monkeypatch.setattr(
        adapter_server,
        "fast_extension_object_exact_match",
        lambda *args, **kwargs: {
            "kind": "Catalog",
            "name": "test2",
            "guid": object_guid,
            "origin": {"extension": {"name": "test2", "guid": extension_guid}},
            "route": {"table": "ConfigCAS", "file_name": "b" * 40},
        },
    )
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_forms",
        lambda payload: {"status": "ok", "forms": []},
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_parts",
        lambda payload: {
            "status": "ok",
            "object": {"kind": "Catalog", "name": "test2", "guid": object_guid},
            "source": {"kind": "live_metadata"},
            "parts": [],
            "counts": {"parts": 0, "roles": {}},
        },
    )

    result = adapter_server.metadata_object_modules(
        {"base_id": "upo_test", "kind": "Catalog", "name": "test2"}
    )

    assert result["status"] == "ok"
    assert len(get_calls) == 1
    assert get_calls[0]["table"] == "ConfigCAS"
    assert get_calls[0]["extension_guid"] == extension_guid
    assert get_calls[0]["file_name"] == "b" * 40


def test_modules_search_object_scoped_extension_skips_global_owner_discovery(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    module_calls: list[dict[str, Any]] = []

    monkeypatch.setattr(
        adapter_server,
        "extension_filter_to_guid",
        lambda base_id, extension, method: (extension_guid, None),
    )
    monkeypatch.setattr(
        adapter_server,
        "extension_definition_guid_sources",
        lambda *args, **kwargs: pytest.fail("global extension owner discovery must be skipped"),
    )

    def fake_modules(payload: dict[str, Any]) -> dict[str, Any]:
        module_calls.append(payload)
        return {
            "status": "ok",
            "object": {"kind": "Catalog", "name": "test2", "guid": "e7e322f5-a7a6-4ddc-bad0-344e75ad00d0"},
            "modules": [],
        }

    monkeypatch.setattr(adapter_server, "metadata_object_modules", fake_modules)
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_commands",
        lambda payload: {"status": "ok", "object_commands": []},
    )

    result = adapter_server.search_modules(
        {
            "base_id": "upo_test",
            "extension": "test2",
            "kind": "Catalog",
            "name": "test2",
            "query": "ЗаменаДомена",
            "state": "active",
        }
    )

    assert result["status"] == "ok"
    assert result["counts"]["matches"] == 0
    assert module_calls[0]["extension_guid"] == extension_guid
    assert module_calls[0]["table"] == "ConfigCAS"


def test_modules_search_working_prefers_saved_extension_form_counterpart(monkeypatch: pytest.MonkeyPatch) -> None:
    active_ref = "ConfigCAS:" + ("a" * 40) + "#form_module"
    saved_ref = "ConfigCASSave:fb26cf42-7609-11f1-828f-005056b0d483__77494708-43ea-4956-ac3c-199cfb035ad2.0#form_module"
    reads: list[tuple[str, str]] = []
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_modules",
        lambda payload: {
            "status": "ok",
            "object": {
                "kind": "Catalog",
                "name": "test2",
                "guid": "e7e322f5-a7a6-4ddc-bad0-344e75ad00d0",
                "origin": {"extension": {"name": "test2", "guid": "fb26cf42-7609-11f1-828f-005056b0d483"}},
            },
            "modules": [
                {
                    "module_id": active_ref,
                    "kind": "form_module",
                    "form": {"name": "t_Форма", "guid": "77494708-43ea-4956-ac3c-199cfb035ad2"},
                }
            ],
        },
    )
    monkeypatch.setattr(adapter_server, "metadata_object_commands", lambda payload: {"status": "ok", "object_commands": []})
    monkeypatch.setattr(
        adapter_server,
        "code_read_saved_counterpart_module_ref",
        lambda base_id, module_ref, payload: saved_ref,
    )

    def fake_read(base_id: str, table: str, file_name: str, **kwargs: Any) -> tuple[bytes, dict[str, Any], None]:
        reads.append((table, file_name))
        text = (
            "Процедура ЗаменаДомена()\n// Отредактировано адаптером\nКонецПроцедуры"
            if table == "ConfigCASSave"
            else "Процедура ЗаменаДомена()\n// active\nКонецПроцедуры"
        )
        return text.encode("utf-8-sig"), {}, None

    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read)

    working = adapter_server.search_modules(
        {
            "base_id": "upo_test",
            "kind": "Catalog",
            "name": "test2",
            "query": "Отредактировано адаптером",
            "state": "working",
        }
    )
    active = adapter_server.search_modules(
        {
            "base_id": "upo_test",
            "kind": "Catalog",
            "name": "test2",
            "query": "Отредактировано адаптером",
            "state": "active",
        }
    )

    assert working["status"] == "ok"
    assert working["matches"][0]["read_selector"]["module_ref"] == saved_ref
    assert working["matches"][0]["activation_state"] == "saved_state"
    assert working["matches"][0]["current_state"] == {"source": "saved_state", "activation_state": "not_activated"}
    assert active["matches"] == []
    assert reads[0][0] == "ConfigCASSave"


def test_metadata_object_commands_enriches_cached_saved_command_with_read_selector(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
    command_guid = "a9b2fc38-86b6-4586-8aca-29b92bdd86de"

    monkeypatch.setattr(
        adapter_server,
        "get_object",
        lambda *args, **kwargs: {"status": "ok", "object": {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid}},
    )
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"database": base_id}, None))
    monkeypatch.setattr(
        adapter_server,
        "metadata_guid_index_lookup_payload",
        lambda config, guid, role: {
            "status": "ok",
            "object": {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid},
            "object_commands": [{"guid": command_guid, "name": "Команда1", "status": "ok", "scope": "object"}],
            "form_commands": [],
            "counts": {"commands": 1, "object_commands": 1, "form_commands": 0},
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_related",
        lambda payload: (_ for _ in ()).throw(AssertionError("cached commands must not trigger a related-object scan")),
    )

    result = adapter_server.metadata_object_commands(
        {
            "base_id": "upo_test",
            "kind": "CalculationRegister",
            "guid": object_guid,
            "table": "ConfigCASSave",
            "extension_guid": extension_guid,
            "include_form_commands": False,
        }
    )

    assert result["status"] == "ok"
    assert result["cache"]["status"] == "hit"
    assert result["object_commands"][0]["read_selector"]["module_ref"] == f"ConfigCASSave:{extension_guid}__{command_guid}.2"


def test_metadata_definition_find_reads_saved_command_routine(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
    command_guid = "a9b2fc38-86b6-4586-8aca-29b92bdd86de"
    module_ref = f"ConfigCASSave:{extension_guid}__{command_guid}.2"
    get_calls: list[dict[str, Any]] = []

    def fake_get_object(kind: str | None, name: str, **kwargs: Any) -> dict[str, Any]:
        get_calls.append(kwargs)
        return {
            "status": "ok",
            "object": {
                "kind": "CalculationRegister",
                "kind_ru": "РегистрРасчета",
                "name": "tt_РегистрРасчета1",
                "guid": object_guid,
            },
        }

    monkeypatch.setattr(adapter_server, "get_object", fake_get_object)
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_modules",
        lambda payload: {"status": "ok", "object": {}, "modules": []},
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_commands",
        lambda payload: {
            "status": "ok",
            "object_commands": [
                {
                    "guid": command_guid,
                    "name": "Команда1",
                    "read_selector": {
                        "method": "modules.read",
                        "base_id": "upo_test",
                        "kind": "CalculationRegister",
                        "guid": object_guid,
                        "module_ref": module_ref,
                    },
                }
            ],
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "read_module",
        lambda payload: {
            "status": "ok",
            "routines": [{"name": "ОбработкаКоманды", "kind": "Процедура"}],
        },
    )

    result = adapter_server.metadata_definition_find(
        {
            "base_id": "upo_test",
            "kind": "CalculationRegister",
            "guid": object_guid,
            "table": "ConfigCASSave",
            "extension_guid": extension_guid,
            "query": "ОбработкаКоманды",
            "areas": ["modules"],
        }
    )

    assert result["status"] == "ok"
    assert result["counts"]["matches"] == 1
    assert result["matches"][0]["name"] == "ОбработкаКоманды"
    assert result["matches"][0]["location"]["command"] == "Команда1"
    assert result["matches"][0]["origin"]["source"] == "extension"
    assert result["matches"][0]["read_selector"]["module_ref"] == module_ref
    assert result["matches"][0]["read_selector"]["routine_name"] == "ОбработкаКоманды"
    assert get_calls[0]["extension_guid"] == extension_guid


def test_metadata_definition_find_accepts_full_code_style_member_path(monkeypatch: pytest.MonkeyPatch) -> None:
    object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
    get_calls: list[tuple[str | None, str]] = []

    def fake_get_object(kind: str | None, name: str, **_kwargs: Any) -> dict[str, Any]:
        get_calls.append((kind, name))
        return {
            "status": "ok",
            "object": {
                "kind": "Document",
                "kind_ru": "Документ",
                "name": "Заказ",
                "guid": object_guid,
            },
        }

    monkeypatch.setattr(adapter_server, "get_object", fake_get_object)
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_attributes",
        lambda _payload: {
            "status": "ok",
            "dimensions": [],
            "resources": [],
            "attributes": [{"name": "Автор", "guid": "11111111-1111-1111-1111-111111111111"}],
            "tabular_sections": [],
        },
    )

    result = adapter_server.metadata_definition_find(
        {
            "base_id": "upo_test",
            "query": "Документы.Заказ.Реквизиты.Автор",
        }
    )

    assert result["status"] == "ok"
    assert get_calls == [("Document", "Заказ")]
    assert result["counts"]["matches"] == 1
    assert result["matches"][0]["area"] == "object"
    assert result["matches"][0]["name"] == "Автор"


def test_metadata_object_parts_uses_saved_extension_storage_prefix(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
    file_name = f"{extension_guid}__{object_guid}"
    list_calls: list[dict[str, Any]] = []

    def fake_list(payload: dict[str, Any]) -> dict[str, Any]:
        list_calls.append(payload)
        return {"status": "ok", "files": [{"FileName": file_name}]}

    monkeypatch.setattr(
        adapter_server,
        "get_object",
        lambda *args, **kwargs: {
            "status": "ok",
            "object": {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid},
        },
    )
    monkeypatch.setattr(adapter_server, "storage_files_list", fake_list)
    monkeypatch.setattr(
        adapter_server,
        "read_storage_files_bytes",
        lambda *args, **kwargs: ({file_name: b"{1}"}, {"database": "upo_test"}, None),
    )

    result = adapter_server.metadata_object_parts(
        {
            "base_id": "upo_test",
            "kind": "CalculationRegister",
            "guid": object_guid,
            "table": "ConfigCASSave",
            "extension_guid": extension_guid,
        }
    )

    assert result["status"] == "ok"
    assert result["counts"]["parts"] == 1
    assert result["object"]["name"] == "tt_РегистрРасчета1"
    assert list_calls[0]["prefix"] == file_name


def test_metadata_object_parts_reads_extension_report_manifest_parts(monkeypatch: pytest.MonkeyPatch) -> None:
    report_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
    manifest_entries = [
        {"suffix": "", "cas_key": "report-descriptor"},
        {"suffix": ".0", "cas_key": "report-part-0"},
        {"suffix": ".1", "cas_key": "report-part-1"},
    ]

    monkeypatch.setattr(adapter_server, "extension_filter_to_guid", lambda *args, **kwargs: ("11111111-1111-1111-1111-111111111111", None))
    monkeypatch.setattr(
        adapter_server,
        "fast_extension_object_exact_match",
        lambda *args, **kwargs: {
            "guid": report_guid,
            "kind": "Report",
            "name": "фс_ПродовольственнаяКорзина",
            "synonym": "Продовольственная корзина",
            "origin": {"source": "extension", "extension": {"name": "фс_Отчеты"}},
            "route": {"table": "ConfigCAS", "file_name": "report-descriptor"},
            "manifest_entries": manifest_entries,
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_files_bytes",
        lambda base_id, table, names, **kwargs: ({name: b"{1}" for name in names}, {"database": "upo_test"}, None),
    )
    monkeypatch.setattr(adapter_server, "resolve_object_guid", lambda *args, **kwargs: pytest.fail("manifest report must not use prefix discovery"))

    result = adapter_server.metadata_object_parts(
        {"base_id": "upo_test", "extension": "фс_Отчеты", "kind": "Report", "name": "фс_ПродовольственнаяКорзина", "include_storage": True}
    )

    assert result["status"] == "ok"
    assert result["source"]["table"] == "ConfigCAS"
    assert result["object"]["kind"] == "Report"
    assert result["counts"]["parts"] == 3
    assert [part["suffix"] for part in result["parts"]] == ["", ".0", ".1"]
    assert result["diagnostics"]["extension_manifest"]["status"] == "resolved"


def test_metadata_object_decode_reads_saved_extension_descriptor(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
    file_name = f"{extension_guid}__{object_guid}"
    read_calls: list[str] = []

    monkeypatch.setattr(adapter_server, "live_dbnames_records", lambda *args, **kwargs: ([], None))
    monkeypatch.setattr(
        adapter_server,
        "get_object",
        lambda *args, **kwargs: {
            "status": "ok",
            "object": {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid},
        },
    )

    def fake_read(base_id: str, table: str, requested_file_name: str, **kwargs: Any) -> tuple[bytes, dict[str, str], None]:
        read_calls.append(requested_file_name)
        return b"{1}", {"database": "upo_test"}, None

    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read)
    monkeypatch.setattr(
        adapter_server,
        "decode_config_object_full",
        lambda *args, **kwargs: {"status": "ok", "root": {}, "semantic": {"sections": []}},
    )

    result = adapter_server.decode_metadata_object(
        {
            "base_id": "upo_test",
            "kind": "CalculationRegister",
            "guid": object_guid,
            "table": "ConfigCASSave",
            "extension_guid": extension_guid,
        }
    )

    assert result["status"] == "ok"
    assert result["source"] == {"kind": "live_metadata"}
    assert result["object"]["name"] == "tt_РегистрРасчета1"
    assert read_calls == [file_name]


def test_metadata_object_full_commands_exposes_saved_command_module_handle(monkeypatch: pytest.MonkeyPatch) -> None:
    object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
    module_ref = "ConfigCASSave:extension__command.2"
    monkeypatch.setattr(
        adapter_server,
        "call_method_impl",
        lambda method, payload: {
            "status": "ok",
            "object": {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid},
            "object_commands": [
                {
                    "name": "Команда1",
                    "read_selector": {
                        "method": "modules.read",
                        "base_id": "upo_test",
                        "kind": "CalculationRegister",
                        "guid": object_guid,
                        "module_ref": module_ref,
                    },
                }
            ],
        },
    )

    result = adapter_server.adapter_full_commands_result(
        {"base_id": "upo_test", "kind": "CalculationRegister", "guid": object_guid},
        {"object": {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid}, "forms": []},
        30,
    )

    assert result["status"] == "ok"
    assert result["counts"]["command_modules"] == 1
    assert result["modules"][0]["kind"] == "command_module"
    assert result["modules"][0]["command"] == "Команда1"
    assert result["modules"][0]["qualified_name"] == "tt_РегистрРасчета1.Команда.Команда1.Модуль команды"
    assert result["modules"][0]["read_selector"]["module_ref"] == module_ref


def test_get_object_resolves_saved_extension_object_by_name(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
    file_name = f"{extension_guid}__{object_guid}"
    monkeypatch.setattr(
        adapter_server,
        "extension_objects_find",
        lambda payload: {
            "status": "ok",
            "objects": [
                {
                    "kind": "CalculationRegister",
                    "name": "tt_РегистрРасчета1",
                    "guid": object_guid,
                    "route": {"table": "ConfigCASSave", "file_name": file_name},
                }
            ],
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda *args, **kwargs: (b"{1}", {"database": "upo_test"}, None),
    )
    monkeypatch.setattr(
        adapter_server,
        "config_identity_from_bytes",
        lambda data: {"guid": object_guid, "name": "tt_РегистрРасчета1", "synonyms": {}},
    )

    result = adapter_server.get_object(
        "CalculationRegister",
        "tt_РегистрРасчета1",
        base_id="upo_test",
        table="ConfigCASSave",
        extension_guid=extension_guid,
        include_semantic=False,
    )

    assert result["status"] == "ok"
    assert result["object"]["name"] == "tt_РегистрРасчета1"
    assert result["object"]["guid"] == object_guid

    rpc_result = adapter_server.call_method(
        "metadata.object.get",
        {
            "base_id": "upo_test",
            "kind": "CalculationRegister",
            "name": "tt_РегистрРасчета1",
            "table": "ConfigCASSave",
            "extension_guid": extension_guid,
        },
    )
    assert rpc_result["status"] == "ok"
    assert rpc_result["object"]["name"] == "tt_РегистрРасчета1"


def test_code_module_owner_cache_stream_ref_falls_back_to_base_ref(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    base_ref = f"ConfigCASSave:{extension_guid}__command.2"
    stream_ref = f"{base_ref}#stream:4"
    lookup_calls: list[str] = []

    monkeypatch.setattr(adapter_server, "metadata_form_owner_cache_lookup", lambda *args, **kwargs: None)

    def fake_lookup(config: dict[str, str], module_ref: str) -> dict[str, Any] | None:
        lookup_calls.append(module_ref)
        if module_ref == base_ref:
            return {
                "owner": {
                    "kind": "CalculationRegister",
                    "name": "tt_РегистрРасчета1",
                    "guid": "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5",
                }
            }
        return None

    monkeypatch.setattr(adapter_server, "metadata_module_owner_cache_lookup", fake_lookup)

    result = adapter_server.code_module_owner_from_cache({"database": "upo_test"}, stream_ref)

    assert lookup_calls == [stream_ref, base_ref]
    assert result["owner_kind"] == "CalculationRegister"
    assert result["owner_name"] == "tt_РегистрРасчета1"
    assert result["extension_guid"] == extension_guid


def test_metadata_code_index_build_ignores_non_bsl_text_streams(monkeypatch: pytest.MonkeyPatch) -> None:
    class FakeStreamClassifier:
        pass

    file_name = "extension__command.2"
    indexed_refs: list[str] = []
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"database": base_id}, None))
    monkeypatch.setattr(adapter_server, "storage_files_list", lambda payload: {"status": "ok", "files": [{"FileName": file_name}]})
    monkeypatch.setattr(
        adapter_server,
        "read_storage_files_bytes",
        lambda *args, **kwargs: ({file_name: b"payload"}, {"database": "upo_test"}, None),
    )
    monkeypatch.setattr(adapter_server, "code_module_owner_from_cache", lambda *args, **kwargs: {})
    monkeypatch.setattr(
        adapter_server,
        "code_index_upsert",
        lambda config, **kwargs: indexed_refs.append(kwargs["module_ref"])
        or {"module_ref": kwargs["module_ref"], "routines": [], "routine_count": 0},
    )
    monkeypatch.setattr(adapter_server, "code_vector_upsert_chunks", lambda *args, **kwargs: 0)
    monkeypatch.setattr(adapter_server, "code_index_prune_file_modules", lambda *args, **kwargs: 0)
    monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
    FakeStreamClassifier.classify_payload = staticmethod(
        lambda data, include_text=True: {
            "stream_blocks": [
                {"text": "служебный descriptor", "has_bsl_marker": False},
                {"text": "Процедура ОбработкаКоманды()\nКонецПроцедуры\n", "has_bsl_marker": True},
            ]
        }
    )

    result = adapter_server.metadata_code_index_build(
        {"base_id": "upo_test", "table": "ConfigCASSave", "prefix": file_name, "include_vectors": False}
    )

    assert result["status"] == "ok"
    assert result["counts"]["indexed"] == 1
    assert indexed_refs == [f"ConfigCASSave:{file_name}#stream:1"]


def test_metadata_code_index_build_routes_named_base_object_to_config(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    captured: dict[str, Any] = {}

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"database": base_id}, None))

    def get_object(kind: str, name: str, **kwargs: Any) -> dict[str, Any]:
        captured.update({"kind": kind, "name": name, "table": kwargs.get("table")})
        return {
            "status": "ok",
            "object": {
                "kind": "CommonModule",
                "name": "ОбменДанными",
                "guid": "11111111-1111-1111-1111-111111111111",
                "ref": "CommonModule.ОбменДанными",
            },
        }

    monkeypatch.setattr(adapter_server, "get_object", get_object)
    monkeypatch.setattr(adapter_server, "metadata_object_modules", lambda payload: {"status": "ok", "modules": []})
    monkeypatch.setattr(adapter_server, "metadata_object_commands", lambda payload: {"status": "ok", "object_commands": []})
    monkeypatch.setattr(adapter_server, "metadata_object_forms", lambda payload: {"status": "ok", "forms": []})

    result = adapter_server.call_method(
        "metadata.code_index.build",
        {
            "base_id": "upo_test",
            "ref": "ОбщиеМодули.ОбменДанными",
            "include_vectors": False,
        },
    )

    assert result["status"] == "ok"
    assert captured == {"kind": "CommonModule", "name": "ОбменДанными", "table": "Config"}
    assert result["source"]["table"] == "Config"
    assert result["query"]["name"] == "ОбменДанными"


def test_metadata_code_index_help_publishes_optional_name_scope() -> None:
    methods = {
        row["name"]: row
        for row in adapter_server.call_method("help.methods", {})["methods"]
    }
    method = methods["metadata.code_index.build"]

    assert method["selector_capabilities"]["selector_purpose"] == "optional_index_scope"
    assert method["selector_capabilities"]["allows_global_search"] is True
    assert method["selector_capabilities"]["accepts_extension"] is True
    assert method["input_schema"]["properties"]["object_name"]["type"] == "string"


def test_form_owner_index_keeps_owner_and_form_names_separate(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    captured: dict[str, Any] = {}

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"database": base_id}, None))

    def find_objects(payload: dict[str, Any]) -> dict[str, Any]:
        captured["find"] = payload
        return {
            "status": "ok",
            "objects": [
                {
                    "kind": "DataProcessor",
                    "name": "ОбменДанными",
                    "guid": "11111111-1111-1111-1111-111111111111",
                    "route": {"table": "ConfigCAS", "file_name": "owner-descriptor"},
                    "origin": {"extension": {"name": "test", "guid": "22222222-2222-2222-2222-222222222222"}},
                }
            ],
        }

    def index_entry(**kwargs: Any) -> tuple[dict[str, Any], None]:
        captured["entry"] = kwargs
        return (
            {
                "owner": {"kind": kwargs["owner_kind"], "name": kwargs["owner_name"]},
                "form": {"name": kwargs["form_name"]},
                "bsl_offset": None,
            },
            None,
        )

    monkeypatch.setattr(adapter_server, "extension_objects_find", find_objects)
    monkeypatch.setattr(adapter_server, "manifest_related_entries_for_cas_key", lambda *args, **kwargs: ([], []))
    monkeypatch.setattr(adapter_server, "form_owner_index_entry_from_sql_payload", index_entry)

    result = adapter_server.call_method(
        "metadata.form.owner_index.build",
        {
            "base_id": "upo_test",
            "ref": "Обработки.ОбменДанными",
            "form_name": "ФормаОбмена",
            "extension": "test",
        },
    )

    assert result["status"] == "ok"
    assert captured["find"]["kind"] == "DataProcessor"
    assert captured["find"]["query"] == "ОбменДанными"
    assert captured["entry"]["owner_name"] == "ОбменДанными"
    assert captured["entry"]["form_name"] == "ФормаОбмена"
    assert result["query"]["owner"] == "ОбменДанными"
    assert result["query"]["form"] == "ФормаОбмена"


def test_form_owner_index_help_publishes_owner_and_form_selectors() -> None:
    methods = {
        row["name"]: row
        for row in adapter_server.call_method("help.methods", {})["methods"]
    }
    method = methods["metadata.form.owner_index.build"]

    assert method["selector_capabilities"]["selector_purpose"] == "optional_form_owner_scope"
    assert method["selector_capabilities"]["form_selector_arguments"] == ["form", "form_name"]
    assert method["input_schema"]["properties"]["form_name"]["type"] == "string"
    assert "Обработки.ОбменДанными" in method["input_schema"]["properties"]["ref"]["description"]


def test_metadata_code_index_build_resolves_saved_command_files_from_object_name(monkeypatch: pytest.MonkeyPatch) -> None:
    class FakeStreamClassifier:
        pass

    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
    command_guid = "a9b2fc38-86b6-4586-8aca-29b92bdd86de"
    file_name = f"{extension_guid}__{command_guid}.2"
    form_file_name = f"{extension_guid}__form-guid.0"
    form_module_ref = f"ConfigCASSave:{form_file_name}#form_module"
    read_chunks: list[list[str]] = []

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"database": base_id}, None))
    monkeypatch.setattr(
        adapter_server,
        "get_object",
        lambda *args, **kwargs: {
            "status": "ok",
            "object": {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid},
        },
    )
    monkeypatch.setattr(adapter_server, "metadata_object_modules", lambda payload: {"status": "ok", "modules": []})
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_commands",
        lambda payload: {
            "status": "ok",
            "object_commands": [
                {
                    "name": "Команда1",
                    "read_selector": {"method": "modules.read", "module_ref": f"ConfigCASSave:{file_name}"},
                }
            ],
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_forms",
        lambda payload: {
            "status": "ok",
            "forms": [{"name": "ФормаСписка", "source": {"table": "ConfigCASSave", "file_name": form_file_name}}],
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_form_decode",
        lambda payload: {
            "status": "ok",
            "profile": {
                "module": {
                    "text": "Процедура ПриОткрытии()\nКонецПроцедуры\n",
                    "read_selector": {"method": "modules.read", "module_ref": form_module_ref},
                }
            },
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: (_ for _ in ()).throw(AssertionError("object-scoped build must not scan storage by prefix")),
    )

    def fake_read(base_id: str, table: str, file_names: list[str], **kwargs: Any) -> tuple[dict[str, bytes], dict[str, str], None]:
        read_chunks.append(file_names)
        return {file_name: b"command", form_file_name: b"form"}, {"database": "upo_test"}, None

    monkeypatch.setattr(adapter_server, "read_storage_files_bytes", fake_read)
    monkeypatch.setattr(adapter_server, "code_module_owner_from_cache", lambda *args, **kwargs: {})
    monkeypatch.setattr(
        adapter_server,
        "code_index_upsert",
        lambda config, **kwargs: {"module_ref": kwargs["module_ref"], "routines": [], "routine_count": 0},
    )
    monkeypatch.setattr(adapter_server, "code_vector_upsert_chunks", lambda *args, **kwargs: 0)
    monkeypatch.setattr(adapter_server, "code_index_prune_file_modules", lambda *args, **kwargs: 0)
    monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
    FakeStreamClassifier.classify_payload = staticmethod(
        lambda data, include_text=True: (
            {"stream_blocks": [{"text": "Процедура ОбработкаКоманды()\nКонецПроцедуры\n", "has_bsl_marker": True}]}
            if data == b"command"
            else {"stream_blocks": []}
        )
    )

    result = adapter_server.metadata_code_index_build(
        {
            "base_id": "upo_test",
            "kind": "CalculationRegister",
            "name": "tt_РегистрРасчета1",
            "extension_guid": extension_guid,
            "include_vectors": False,
        }
    )

    assert result["status"] == "ok"
    assert result["query"]["table"] == "ConfigCASSave"
    assert result["object"]["name"] == "tt_РегистрРасчета1"
    assert result["counts"]["discovered_module_refs"] == 2
    assert result["counts"]["forms_scanned"] == 1
    assert result["counts"]["form_modules_discovered"] == 1
    assert result["counts"]["empty_form_modules"] == 0
    assert result["counts"]["form_module_errors"] == 0
    assert result["counts"]["indexed"] == 2
    assert read_chunks == [[file_name, form_file_name]]


def test_extension_objects_find_active_state_excludes_saved_state(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"database": base_id}, None))
    monkeypatch.setattr(adapter_server, "extension_filter_to_guid", lambda base_id, extension, method: (extension_guid, None))
    monkeypatch.setattr(adapter_server, "extension_route_cache_lookup", lambda *args, **kwargs: [])
    monkeypatch.setattr(
        adapter_server,
        "extension_manifest_object_matches",
        lambda *args, **kwargs: ([], [], {"manifest_count": 0, "descriptor_entries": 0, "descriptor_payloads_read": 0}),
    )

    def fail_saved_state_search(payload: dict[str, Any]) -> dict[str, Any]:
        raise AssertionError("state=active must not inspect ConfigCASSave")

    monkeypatch.setattr(adapter_server, "metadata_saved_state_forms_search", fail_saved_state_search)

    result = adapter_server.extension_objects_find({"base_id": "upo_test", "extension": "test2", "state": "active", "limit": 50})

    assert result["status"] == "not_found"
    assert result["query"]["state"] == "active"
    assert result["counts"]["saved_state_matches"] == 0


def test_extension_cache_rebuild_warms_route_cache(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
    config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
    descriptor_key = "a" * 40
    part_key = "b" * 40
    object_base_id = "7c6dc711-02f5-4e6a-9f37-7dd57d8942b3"
    guid = "11111111-2222-3333-4444-555555555555"
    name = "\u0444\u0441_\u041f\u0424_MXL_\u041c\u0435\u043d\u044e\u0417\u0430\u041f\u0435\u0440\u0438\u043e\u0434"
    descriptor_payload = f'{{1,{{3,{{1,0,{guid}}},"{name}",{{1,"ru","Test template"}}}}}}'.encode("utf-8")

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    monkeypatch.setattr(adapter_server, "extension_definition_guid_sources", lambda *args, **kwargs: ({}, None))
    monkeypatch.setattr(
        adapter_server,
        "live_extension_manifests",
        lambda *args, **kwargs: (
            [
                {
                    "root_cas_key": "c" * 40,
                    "extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"},
                    "entries": [
                        {"object_id": object_base_id, "object_base_id": object_base_id, "suffix": "", "cas_key": descriptor_key},
                        {"object_id": f"{object_base_id}.0", "object_base_id": object_base_id, "suffix": ".0", "cas_key": part_key},
                    ],
                }
            ],
            [],
        ),
    )
    read_calls = {"count": 0}

    def fake_read_files(*args: Any, **kwargs: Any) -> tuple[dict[str, bytes], dict[str, str], None]:
        read_calls["count"] += 1
        return {descriptor_key: descriptor_payload}, config, None

    monkeypatch.setattr(adapter_server, "read_storage_files_bytes", fake_read_files)

    rebuild = adapter_server.extension_cache_rebuild({"base_id": "upo_test", "kind": "Template", "max_items": 10})
    assert rebuild["status"] == "ok"
    assert rebuild["counts"]["cached_routes"] == 1

    def fail_dbnames_lookup(*args: Any, **kwargs: Any) -> tuple[dict[str, list[dict[str, Any]]], None]:
        raise AssertionError("DBNames lookup must not run for a validated source-cache hit")

    monkeypatch.setattr(adapter_server, "extension_definition_guid_sources", fail_dbnames_lookup)
    found = adapter_server.extension_objects_find({"base_id": "upo_test", "kind": "Template", "query": name, "limit": 1})
    assert found["status"] == "ok"
    assert found["objects"][0]["match_by"] == "source_cache"
    assert found["counts"]["source_cache_validated"] == 1
    assert read_calls["count"] == 1


def test_extension_cache_validate_marks_missing_routes_stale(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
    config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
    fresh_key = "a" * 40
    stale_key = "b" * 40
    object_base_id = "7c6dc711-02f5-4e6a-9f37-7dd57d8942b3"

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    manifest_calls = {"count": 0}

    def fake_live_extension_manifests(*args: Any, **kwargs: Any) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
        manifest_calls["count"] += 1
        return (
            [
                {
                    "root_cas_key": "c" * 40,
                    "extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"},
                    "entries": [
                        {"object_id": object_base_id, "object_base_id": object_base_id, "suffix": "", "cas_key": fresh_key},
                    ],
                }
            ],
            [],
        )

    monkeypatch.setattr(adapter_server, "live_extension_manifests", fake_live_extension_manifests)
    for key, name in ((fresh_key, "FreshTemplate"), (stale_key, "StaleTemplate")):
        adapter_server.extension_route_cache_upsert(
            config,
            {
                "kind": "Template",
                "kind_ru": "Макет",
                "name": name,
                "guid": key,
                "origin": {"extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"}},
                "route": {
                    "route_type": "extension_manifest_cas",
                    "table": "ConfigCAS",
                    "file_name": key,
                    "manifest_entry": {
                        "object_id": object_base_id,
                        "object_base_id": object_base_id,
                        "suffix": "",
                        "cas_key": key,
                        "extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"},
                        "root_cas_key": "c" * 40,
                    },
                },
                "manifest_entries": [],
            },
            descriptor_payload_sha1="sha",
        )

    result = adapter_server.extension_cache_validate({"base_id": "upo_test", "include_entries": True})

    assert result["status"] == "ok"
    assert result["counts"] == {"checked": 2, "fresh": 1, "stale": 1}
    assert manifest_calls["count"] == 1
    stale_entry = next(item for item in result["entries"] if item["descriptor_cas_key"] == stale_key)
    assert stale_entry["freshness"]["status"] == "stale"


def test_extension_cache_status_groups_freshness(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
    config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
    fresh_key = "a" * 40
    stale_key = "b" * 40
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))

    for key, name in ((fresh_key, "FreshTemplate"), (stale_key, "StaleTemplate")):
        adapter_server.extension_route_cache_upsert(
            config,
            {
                "kind": "Template",
                "kind_ru": "Макет",
                "name": name,
                "guid": key,
                "origin": {"extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"}},
                "route": {
                    "route_type": "extension_manifest_cas",
                    "table": "ConfigCAS",
                    "file_name": key,
                    "manifest_entry": {
                        "object_id": key,
                        "object_base_id": key,
                        "suffix": "",
                        "cas_key": key,
                        "extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"},
                    },
                },
                "manifest_entries": [],
            },
            descriptor_payload_sha1="sha",
        )
    adapter_server.extension_route_cache_mark_stale(config, stale_key, "test_stale")

    result = adapter_server.extension_cache_status({"base_id": "upo_test", "include_entries": True, "limit": 10})

    assert result["status"] == "ok"
    assert result["counts"]["total"] == 2
    assert result["counts"]["usable"] == 1
    assert result["counts"]["stale"] == 1
    assert result["groups"][0]["counts"] == {"total": 2, "usable": 1, "stale": 1}
    assert result["entries"][0]["freshness_status"] == "stale"
    assert result["entries"][0]["stale_reason"] == "test_stale"


def test_extension_objects_find_reuses_recent_validated_cache_with_ttl(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
    config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
    descriptor_key = "a" * 40
    name = "\u0444\u0441_\u041f\u0424_MXL_\u041c\u0435\u043d\u044e\u0417\u0430\u041f\u0435\u0440\u0438\u043e\u0434"

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    adapter_server.extension_route_cache_upsert(
        config,
        {
            "kind": "Template",
            "kind_ru": "Макет",
            "name": name,
            "guid": descriptor_key,
            "origin": {"extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"}},
            "route": {"route_type": "extension_manifest_cas", "table": "ConfigCAS", "file_name": descriptor_key},
            "manifest_entries": [],
        },
        descriptor_payload_sha1="sha",
    )
    manifest_calls = {"count": 0}

    def fake_live_extension_manifests(*args: Any, **kwargs: Any) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
        manifest_calls["count"] += 1
        return ([], [])

    monkeypatch.setattr(adapter_server, "live_extension_manifests", fake_live_extension_manifests)
    monkeypatch.setattr(adapter_server, "extension_definition_guid_sources", lambda *args, **kwargs: ({}, None))
    monkeypatch.setattr(adapter_server, "storage_files_list", lambda payload: {"status": "ok", "files": []})

    recent = adapter_server.extension_objects_find({"base_id": "upo_test", "kind": "Template", "query": name, "limit": 1, "cache_ttl_seconds": 300})
    assert recent["status"] == "ok"
    assert recent["objects"][0]["freshness"]["validated_by"] == "recent_source_cache"
    assert manifest_calls["count"] == 0

    expired = adapter_server.extension_objects_find({"base_id": "upo_test", "kind": "Template", "query": name, "limit": 1, "cache_ttl_seconds": 0, "scan_limit": 1})
    assert expired["status"] == "not_found"
    assert expired["counts"]["source_cache_stale"] == 1
    assert manifest_calls["count"] >= 1


def test_extension_objects_find_marks_stale_source_cache(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
    config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
    descriptor_key = "a" * 40
    name = "\u0444\u0441_\u041f\u0424_MXL_\u041c\u0435\u043d\u044e\u0417\u0430\u041f\u0435\u0440\u0438\u043e\u0434"

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    monkeypatch.setattr(adapter_server, "extension_definition_guid_sources", lambda *args, **kwargs: ({}, None))
    monkeypatch.setattr(adapter_server, "live_extension_manifests", lambda *args, **kwargs: ([], []))
    monkeypatch.setattr(adapter_server, "storage_files_list", lambda payload: {"status": "ok", "files": []})

    adapter_server.extension_route_cache_upsert(
        config,
        {
            "kind": "Template",
            "kind_ru": "Макет",
            "name": name,
            "guid": descriptor_key,
            "origin": {"extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"}},
            "route": {"route_type": "extension_manifest_cas", "table": "ConfigCAS", "file_name": descriptor_key},
            "manifest_entries": [],
        },
        descriptor_payload_sha1="sha",
    )

    result = adapter_server.extension_objects_find({"base_id": "upo_test", "kind": "Template", "query": name, "limit": 1, "scan_limit": 1, "cache_ttl_seconds": 0})

    assert result["status"] == "not_found"
    assert result["counts"]["source_cache_candidates"] == 1
    assert result["counts"]["source_cache_stale"] == 1


def test_template_part_structure_reuses_decoded_artifact_cache(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
    config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
    payload = b"MOXCEL cached payload"
    calls = {"decode": 0}

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (payload, config, None))

    def fake_extract(data: bytes) -> dict[str, Any]:
        calls["decode"] += 1
        return {
            "format": "MOXCEL",
            "dimensions": {"rows": 1, "columns": 1},
            "named_areas": [{"name": "ОбластьШапка", "range": {"one_based": {"top": 1, "left": 1, "bottom": 1, "right": 1}}}],
            "parameters": [{"name": "Период"}],
            "cells": [{"row": 1, "column": 1, "text": "Текст"}],
            "capabilities": {"decoded_binary": True, "named_areas": True},
        }

    monkeypatch.setattr(adapter_server, "extract_moxel_public_structure", fake_extract)
    part = {
        "part_id": "part-key",
        "table": "ConfigCAS",
        "classification": {"markers": ["MOXCEL"], "role": "template_payload"},
    }

    first = adapter_server.template_part_structure("upo_test", part, timeout_seconds=30)
    second = adapter_server.template_part_structure("upo_test", part, timeout_seconds=30)

    assert calls["decode"] == 1
    assert first["artifact_cache"]["status"] == "miss_stored"
    assert second["artifact_cache"]["status"] == "hit"
    assert second["structure"]["named_areas"][0]["name"] == "ОбластьШапка"


def test_bounded_template_content_export_extracts_html_from_base64_block() -> None:
    html = b"<!DOCTYPE html><html><body>Template help</body></html>"
    encoded = base64.b64encode(html).decode("ascii")
    stored = compress_payload(f'{{5,1,"ru",{{#base64:{encoded}}}}}'.encode("utf-8-sig"), "raw_deflate")

    result = adapter_server.bounded_template_content_export(stored, max_content_bytes=4096)

    assert result["status"] == "complete"
    assert result["container"]["encoding"] == "base64"
    assert result["container"]["truncated"] is False
    assert result["counts"]["extracted_text"] == 1
    assert result["extracted_text"][0]["media_type"] == "text/html"
    assert "Template help" in result["extracted_text"][0]["text"]


def test_bounded_template_content_export_limits_binary_payload() -> None:
    stored = compress_payload(b"\x00\x01\x02\x03\x04\x05", "raw_deflate")

    result = adapter_server.bounded_template_content_export(stored, max_content_bytes=4)

    assert result["status"] == "truncated"
    assert result["container"]["bytes"] == 6
    assert result["container"]["returned_bytes"] == 4
    assert base64.b64decode(result["container"]["data_base64"]) == b"\x00\x01\x02\x03"


def test_template_part_structure_exports_non_moxel_content_only_when_requested(monkeypatch: pytest.MonkeyPatch) -> None:
    payload = compress_payload(b"plain template content", "raw_deflate")
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (payload, {}, None))
    part = {"part_id": "part-key", "table": "Config", "classification": {"markers": [], "role": "binary_or_unknown_payload"}}

    without_content = adapter_server.template_part_structure("upo_test", part, timeout_seconds=30)
    with_content = adapter_server.template_part_structure("upo_test", part, timeout_seconds=30, include_content=True, max_content_bytes=100)

    assert "content_export" not in without_content
    assert with_content["content_export"]["status"] == "complete"
    assert base64.b64decode(with_content["content_export"]["container"]["data_base64"]) == b"plain template content"


def test_templates_read_content_export_arguments_are_strict_and_bounded() -> None:
    selector = {"base_id": "upo_test", "route_ref": "ConfigCAS:" + ("a" * 40)}

    assert adapter_server.validate_templates_read_payload({**selector, "include_content": True, "max_content_bytes": 4096}) is None
    invalid_bool = adapter_server.validate_templates_read_payload({**selector, "include_content": "yes"})
    too_large = adapter_server.validate_templates_read_payload({**selector, "max_content_bytes": adapter_server.TEMPLATE_CONTENT_MAX_BYTES + 1})

    assert invalid_bool is not None and invalid_bool["argument"] == "include_content"
    assert too_large is not None and too_large["argument"] == "max_content_bytes"


def test_templates_read_job_validation_uses_template_response_view_values() -> None:
    payload = {"base_id": "upo_test", "route_ref": "ConfigCAS:" + ("a" * 40), "view": "full"}

    assert adapter_server.validate_adapter_job_payload("templates.read", payload) is None


def test_templates_read_rpc_validation_uses_template_response_view_values(monkeypatch: pytest.MonkeyPatch) -> None:
    descriptor_key = "a" * 40
    monkeypatch.setattr(
        adapter_server,
        "read_template_by_route",
        lambda payload, analyze=False: {"status": "ok", "view": payload.get("view"), "templates": [], "counts": {"templates": 0}},
    )

    result = adapter_server.call_method(
        "templates.read",
        {"base_id": "upo_test", "route_ref": f"ConfigCAS:{descriptor_key}", "view": "full"},
    )

    assert result["status"] == "ok"
    assert result["view"] == "full"


def test_templates_read_does_not_forward_response_view_to_metadata_selector(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: list[dict[str, Any]] = []

    def fake_details(payload: dict[str, Any]) -> dict[str, Any]:
        seen.append(payload)
        return {"status": "ok", "source": {"kind": "test"}, "object": {"kind": "Document", "name": "Doc"}, "templates": []}

    monkeypatch.setattr(adapter_server, "metadata_object_template_details", fake_details)
    result = adapter_server.templates_read({"base_id": "upo_test", "kind": "Document", "name": "Doc", "view": "full"})

    assert result["status"] == "ok"
    assert result["view"] == "full"
    assert "view" not in seen[0]


def test_templates_areas_find_returns_named_area_coordinates_for_extension_template(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
    config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
    template_name = "фс_ПФ_MXL_МенюЗаПериод"
    descriptor_key = "a" * 40
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))

    def fake_extension_objects_find(payload: dict[str, Any]) -> dict[str, Any]:
        assert payload["query"] == template_name
        assert payload["kind"] == "Template"
        return {
            "status": "ok",
            "objects": [
                {
                    "kind": "Template",
                    "name": template_name,
                    "guid": descriptor_key,
                    "match_by": "source_cache",
                    "origin": {"extension": {"name": "фс_Отчеты", "guid": "99999999-9999-9999-9999-999999999999"}},
                    "route": {"route_type": "extension_manifest_cas", "table": "ConfigCAS", "file_name": descriptor_key},
                    "freshness": {"status": "fresh", "validated_by": "live_manifest", "validation_required": False},
                }
            ],
        }

    def fake_read_template_by_route(payload: dict[str, Any], *, analyze: bool = False) -> dict[str, Any]:
        assert payload["file_name"] == descriptor_key
        assert payload["sections"] == "named_areas,coverage,diagnostics"
        return {
            "status": "ok",
            "templates": [
                {
                    "name": template_name,
                    "guid": descriptor_key,
                    "structure": {
                        "format": "MOXCEL",
                        "dimensions": {"rows": 20, "columns": 8},
                        "capabilities": {"named_areas": True, "named_area_coordinates": True},
                        "named_areas": [
                            {
                                "name": "ОбластьШапка",
                                "occurrence": 1,
                                "source": "moxel_named_area",
                                "range": {
                                    "address": "R1C1:R3C8",
                                    "one_based": {"top": 1, "left": 1, "bottom": 3, "right": 8},
                                    "zero_based": {"top": 0, "left": 0, "bottom": 2, "right": 7},
                                    "width": 8,
                                    "height": 3,
                                },
                            }
                        ],
                        "area_cell_coverage": [
                            {
                                "name": "ОбластьШапка",
                                "occurrence": 1,
                                "cell_count": 1,
                                "parameter_count": 1,
                                "cells": [{"row": 1, "column": 1, "text": "Меню за период"}],
                                "parameters": [{"name": "Период", "row": 2, "column": 1, "source": "cell_parameter"}],
                            }
                        ],
                    },
                }
            ],
            "counts": {"templates": 1, "parts": 1},
        }

    monkeypatch.setattr(adapter_server, "extension_objects_find", fake_extension_objects_find)
    monkeypatch.setattr(adapter_server, "read_template_by_route", fake_read_template_by_route)

    result = adapter_server.templates_areas_find(
        {"base_id": "upo_test", "extension": "фс_Отчеты", "query": template_name, "max_areas": 50}
    )

    assert result["status"] == "ok"
    assert result["source"]["authoritative"] is True
    assert result["counts"]["templates"] == 1
    assert result["counts"]["areas"] == 1
    assert result["counts"]["areas_with_coordinates"] == 1
    assert result["templates"][0]["object"]["name"] == template_name
    assert result["templates"][0]["route"]["route_ref"] == f"ConfigCAS:{descriptor_key}"
    assert result["templates"][0]["read_selector"]["route_ref"] == f"ConfigCAS:{descriptor_key}"
    area = result["areas"][0]
    assert area["name"] == "ОбластьШапка"
    assert area["coordinates_available"] is True
    assert area["range"]["one_based"] == {"top": 1, "left": 1, "bottom": 3, "right": 8}
    assert area["coverage"]["parameters"][0]["name"] == "Период"


def test_templates_areas_find_retries_query_without_short_prefix(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
    descriptor_key = "a" * 40
    attempts: list[dict[str, Any]] = []

    def fake_extension_objects_find(payload: dict[str, Any]) -> dict[str, Any]:
        attempts.append(payload)
        if payload.get("query") == "ПФ_MXL_МенюЗаПериод" and "kind" not in payload:
            return {
                "status": "ok",
                "objects": [
                    {
                        "name": "ПФ_MXL_МенюЗаПериод",
                        "guid": descriptor_key,
                        "route": {"route_type": "configcas_payload", "table": "ConfigCAS", "file_name": descriptor_key},
                    }
                ],
            }
        return {"status": "not_found", "objects": [], "diagnostics": []}

    monkeypatch.setattr(adapter_server, "extension_objects_find", fake_extension_objects_find)
    monkeypatch.setattr(
        adapter_server,
        "read_template_by_route",
        lambda payload, analyze=False: {
            "status": "ok",
            "templates": [
                {
                    "name": "ПФ_MXL_МенюЗаПериод",
                    "guid": descriptor_key,
                    "structure": {
                        "format": "MOXCEL",
                        "capabilities": {"named_area_coordinates": True},
                        "named_areas": [
                            {
                                "name": "ОбластьШапка",
                                "occurrence": 1,
                                "range": {"one_based": {"top": 1, "left": 1, "bottom": 1, "right": 4}},
                            }
                        ],
                    },
                }
            ],
            "counts": {"templates": 1, "parts": 1},
        },
    )

    result = adapter_server.templates_areas_find({"base_id": "upo_test", "query": "фс_ПФ_MXL_МенюЗаПериод"})

    assert result["status"] == "ok"
    assert [attempt.get("query") for attempt in attempts] == [
        "фс_ПФ_MXL_МенюЗаПериод",
        "фс_ПФ_MXL_МенюЗаПериод",
        "ПФ_MXL_МенюЗаПериод",
        "ПФ_MXL_МенюЗаПериод",
    ]
    assert result["areas"][0]["name"] == "ОбластьШапка"
    assert result["templates"][0]["route"]["route_ref"] == f"ConfigCAS:{descriptor_key}"


def test_templates_areas_find_accepts_route_ref(monkeypatch: pytest.MonkeyPatch) -> None:
    descriptor_key = "a" * 40
    seen_payloads: list[dict[str, Any]] = []

    def fake_read_template_by_route(payload: dict[str, Any], *, analyze: bool = False) -> dict[str, Any]:
        seen_payloads.append(payload)
        return {
            "status": "ok",
            "templates": [
                {
                    "name": "RouteRefTemplate",
                    "guid": descriptor_key,
                    "structure": {
                        "format": "MOXCEL",
                        "capabilities": {"named_area_coordinates": True},
                        "named_areas": [
                            {
                                "name": "ОбластьШапка",
                                "occurrence": 1,
                                "range": {"one_based": {"top": 1, "left": 1, "bottom": 2, "right": 4}},
                            }
                        ],
                    },
                }
            ],
            "counts": {"templates": 1, "parts": 1},
        }

    monkeypatch.setattr(adapter_server, "read_template_by_route", fake_read_template_by_route)

    result = adapter_server.templates_areas_find({"base_id": "upo_test", "route_ref": f"ConfigCAS:{descriptor_key}"})

    assert result["status"] == "ok"
    assert seen_payloads[0]["table"] == "ConfigCAS"
    assert seen_payloads[0]["file_name"] == descriptor_key
    assert result["templates"][0]["route"]["route_ref"] == f"ConfigCAS:{descriptor_key}"
    assert result["areas"][0]["name"] == "ОбластьШапка"


def test_templates_areas_find_normalizes_public_template_ref(monkeypatch: pytest.MonkeyPatch) -> None:
    descriptor_key = "a" * 40
    seen: dict[str, Any] = {}

    def fake_find(payload: dict[str, Any]) -> dict[str, Any]:
        seen["find_payload"] = payload
        return {
            "status": "ok",
            "objects": [
                {
                    "object": {"kind": "Template", "name": "ПечатнаяФорма", "guid": descriptor_key},
                    "route": {"table": "ConfigCAS", "file_name": descriptor_key, "route_type": "cache"},
                    "match_by": "name",
                }
            ],
        }

    monkeypatch.setattr(adapter_server, "extension_objects_find", fake_find)
    monkeypatch.setattr(
        adapter_server,
        "read_template_by_route",
        lambda payload, analyze=False: {
            "status": "ok",
            "templates": [
                {
                    "name": "ПечатнаяФорма",
                    "guid": descriptor_key,
                    "structure": {
                        "format": "MOXCEL",
                        "named_areas": [
                            {
                                "name": "Шапка",
                                "occurrence": 1,
                                "range": {"one_based": {"top": 1, "left": 1, "bottom": 1, "right": 4}},
                            }
                        ],
                    },
                }
            ],
        },
    )

    result = adapter_server.call_method(
        "templates.areas.find",
        {
            "base_id": "upo_test",
            "ref": "Макеты.ПечатнаяФорма",
            "area_name": "Шапка",
        },
    )

    assert result["status"] == "ok"
    assert seen["find_payload"]["query"] == "ПечатнаяФорма"
    assert seen["find_payload"]["kind"] == "Template"
    assert result["query"]["query"] == "ПечатнаяФорма"
    assert result["query"]["area_query"] == "Шапка"
    assert result["areas"][0]["name"] == "Шапка"


def test_templates_areas_find_help_separates_template_and_area_selectors() -> None:
    method = {
        row["name"]: row
        for row in adapter_server.call_method("help.methods", {})["methods"]
    }["templates.areas.find"]

    assert method["selector_capabilities"]["accepts_ordinal"] is False
    assert method["selector_capabilities"]["read_only"] is True
    assert method["selector_capabilities"]["child_selector_arguments"] == [
        "area_name",
        "area_query",
        "area_occurrence",
    ]
    assert method["selector_capabilities"]["direct_route_arguments"] == ["route_ref", "table", "file_name"]
    assert method["input_schema"]["properties"]["ref"]["type"] == "string"
    assert method["input_schema"]["properties"]["area_name"]["type"] == "string"


def test_metadata_cache_lookup_normalizes_public_object_ref(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_lookup(base_id: str, kind: str, name: str) -> dict[str, Any]:
        seen["lookup"] = {"base_id": base_id, "kind": kind, "name": name}
        return {
            "base_id": base_id,
            "kind": kind,
            "kind_ru": "Документ",
            "name": name,
            "guid": "document-guid",
            "source": "base",
        }

    monkeypatch.setattr(adapter_server, "metadata_cache_lookup_row", fake_lookup)

    result = adapter_server.call_method(
        "metadata.cache.lookup",
        {
            "base_id": "upo_test",
            "ref": "Документы.ЗаказКлиента",
        },
    )

    assert result["status"] == "ok"
    assert seen["lookup"] == {
        "base_id": "upo_test",
        "kind": "Document",
        "name": "ЗаказКлиента",
    }
    assert result["object"]["name"] == "ЗаказКлиента"


def test_metadata_cache_lookup_help_marks_cache_as_non_authoritative() -> None:
    method = {
        row["name"]: row
        for row in adapter_server.call_method("help.methods", {})["methods"]
    }["metadata.cache.lookup"]

    assert method["selector_capabilities"]["accepts_ordinal"] is False
    assert method["selector_capabilities"]["requires_object_selector"] is True
    assert method["selector_capabilities"]["read_only"] is True
    assert method["selector_capabilities"]["authoritative_source"] == "live_sql_not_cache"
    assert method["input_schema"]["properties"]["ref"]["type"] == "string"


def test_templates_areas_find_filters_by_area_query(monkeypatch: pytest.MonkeyPatch) -> None:
    descriptor_key = "a" * 40
    seen_payloads: list[dict[str, Any]] = []

    def fake_read_template_by_route(payload: dict[str, Any], *, analyze: bool = False) -> dict[str, Any]:
        seen_payloads.append(payload)
        return {
            "status": "ok",
            "templates": [
                {
                    "name": "RouteRefTemplate",
                    "guid": descriptor_key,
                    "structure": {
                        "format": "MOXCEL",
                        "capabilities": {"named_area_coordinates": True},
                        "named_areas": [
                            {
                                "name": "ОбластьШапка",
                                "occurrence": 1,
                                "range": {"one_based": {"top": 1, "left": 1, "bottom": 2, "right": 4}},
                            },
                            {
                                "name": "ОбластьТело",
                                "occurrence": 1,
                                "range": {"one_based": {"top": 3, "left": 1, "bottom": 5, "right": 4}},
                            },
                        ],
                    },
                }
            ],
            "counts": {"templates": 1, "parts": 1},
        }

    monkeypatch.setattr(adapter_server, "read_template_by_route", fake_read_template_by_route)

    result = adapter_server.templates_areas_find(
        {"base_id": "upo_test", "route_ref": f"ConfigCAS:{descriptor_key}", "area_query": "Тело", "max_areas": 1}
    )

    assert result["status"] == "ok"
    assert seen_payloads[0]["max_areas"] == 5000
    assert result["query"]["area_query"] == "Тело"
    assert result["counts"]["area_filter_applied"] is True
    assert result["counts"]["decoded_named_areas"] == 2
    assert result["counts"]["areas"] == 1
    assert result["counts"]["matching_areas"] == 1
    assert result["counts"]["areas_limited"] is False
    assert result["areas"][0]["name"] == "ОбластьТело"


def test_templates_areas_find_reports_matching_areas_before_limit(monkeypatch: pytest.MonkeyPatch) -> None:
    descriptor_key = "a" * 40

    monkeypatch.setattr(
        adapter_server,
        "read_template_by_route",
        lambda payload, analyze=False: {
            "status": "ok",
            "templates": [
                {
                    "name": "RouteRefTemplate",
                    "guid": descriptor_key,
                    "structure": {
                        "format": "MOXCEL",
                        "capabilities": {"named_area_coordinates": True},
                        "named_areas": [
                            {"name": "ОбластьШапка", "occurrence": 1, "range": {"one_based": {"top": 1, "left": 1, "bottom": 2, "right": 4}}},
                            {"name": "ОбластьТело", "occurrence": 1, "range": {"one_based": {"top": 3, "left": 1, "bottom": 5, "right": 4}}},
                            {"name": "ОбластьПодвал", "occurrence": 1, "range": {"one_based": {"top": 6, "left": 1, "bottom": 6, "right": 4}}},
                        ],
                    },
                }
            ],
            "counts": {"templates": 1, "parts": 1},
        },
    )

    result = adapter_server.templates_areas_find(
        {"base_id": "upo_test", "route_ref": f"ConfigCAS:{descriptor_key}", "area_query": "Область", "max_areas": 1}
    )

    assert result["status"] == "ok"
    assert result["counts"]["decoded_named_areas"] == 3
    assert result["counts"]["areas"] == 1
    assert result["counts"]["areas_returned"] == 1
    assert result["counts"]["matching_areas"] == 3
    assert result["counts"]["areas_limited"] is True
    assert result["templates"][0]["counts"]["matching_areas"] == 3
    assert result["templates"][0]["counts"]["areas_limited"] is True


def test_templates_areas_find_can_skip_coverage(monkeypatch: pytest.MonkeyPatch) -> None:
    descriptor_key = "a" * 40
    seen_payloads: list[dict[str, Any]] = []

    def fake_read_template_by_route(payload: dict[str, Any], *, analyze: bool = False) -> dict[str, Any]:
        seen_payloads.append(payload)
        return {
            "status": "ok",
            "templates": [
                {
                    "name": "RouteRefTemplate",
                    "guid": descriptor_key,
                    "structure": {
                        "format": "MOXCEL",
                        "capabilities": {"named_area_coordinates": True},
                        "named_areas": [
                            {"name": "Header", "occurrence": 1, "range": {"one_based": {"top": 1, "left": 1, "bottom": 2, "right": 4}}},
                        ],
                        "area_cell_coverage": [
                            {
                                "name": "Header",
                                "occurrence": 1,
                                "cell_count": 2,
                                "parameter_count": 1,
                                "cells": [{"row": 1, "column": 1}],
                                "parameters": [{"name": "Param"}],
                            }
                        ],
                    },
                }
            ],
            "counts": {"templates": 1, "parts": 1},
        }

    monkeypatch.setattr(adapter_server, "read_template_by_route", fake_read_template_by_route)

    result = adapter_server.templates_areas_find(
        {"base_id": "upo_test", "route_ref": f"ConfigCAS:{descriptor_key}", "include_coverage": False}
    )

    assert result["status"] == "ok"
    assert seen_payloads[0]["sections"] == "named_areas,diagnostics"
    assert seen_payloads[0]["max_coverage"] == 0
    assert result["query"]["include_coverage"] is False
    assert "coverage" not in result["areas"][0]


def test_templates_areas_find_area_name_defaults_to_exact_match(monkeypatch: pytest.MonkeyPatch) -> None:
    descriptor_key = "a" * 40
    monkeypatch.setattr(
        adapter_server,
        "read_template_by_route",
        lambda payload, analyze=False: {
            "status": "ok",
            "templates": [
                {
                    "name": "RouteRefTemplate",
                    "guid": descriptor_key,
                    "structure": {
                        "format": "MOXCEL",
                        "capabilities": {"named_area_coordinates": True},
                        "named_areas": [
                            {
                                "name": "ОбластьШапка",
                                "occurrence": 1,
                                "range": {"one_based": {"top": 1, "left": 1, "bottom": 2, "right": 4}},
                            },
                            {
                                "name": "ОбластьШапкаЦена",
                                "occurrence": 1,
                                "range": {"one_based": {"top": 3, "left": 1, "bottom": 3, "right": 4}},
                            },
                        ],
                    },
                }
            ],
            "counts": {"templates": 1, "parts": 1},
        },
    )

    result = adapter_server.templates_areas_find(
        {"base_id": "upo_test", "route_ref": f"ConfigCAS:{descriptor_key}", "area_name": "ОбластьШапка"}
    )

    assert result["status"] == "ok"
    assert result["query"]["area_match"] == "exact"
    assert result["counts"]["decoded_named_areas"] == 2
    assert result["counts"]["areas"] == 1
    assert result["areas"][0]["name"] == "ОбластьШапка"


def test_templates_areas_find_filters_by_area_occurrence(monkeypatch: pytest.MonkeyPatch) -> None:
    descriptor_key = "a" * 40
    seen_payloads: list[dict[str, Any]] = []

    def fake_read_template_by_route(payload: dict[str, Any], *, analyze: bool = False) -> dict[str, Any]:
        seen_payloads.append(payload)
        return {
            "status": "ok",
            "templates": [
                {
                    "name": "RouteRefTemplate",
                    "guid": descriptor_key,
                    "structure": {
                        "format": "MOXCEL",
                        "capabilities": {"named_area_coordinates": True},
                        "named_areas": [
                            {
                                "name": "ОбластьШапка",
                                "occurrence": 1,
                                "range": {"one_based": {"top": 1, "left": 1, "bottom": 2, "right": 4}},
                            },
                            {
                                "name": "ОбластьШапка",
                                "occurrence": 2,
                                "range": {"one_based": {"top": 1, "left": 1, "bottom": 3, "right": 4}},
                            },
                            {
                                "name": "ОбластьШапкаЦена",
                                "occurrence": 1,
                                "range": {"one_based": {"top": 4, "left": 1, "bottom": 4, "right": 4}},
                            },
                        ],
                    },
                }
            ],
            "counts": {"templates": 1, "parts": 1},
        }

    monkeypatch.setattr(adapter_server, "read_template_by_route", fake_read_template_by_route)

    result = adapter_server.templates_areas_find(
        {
            "base_id": "upo_test",
            "route_ref": f"ConfigCAS:{descriptor_key}",
            "area_name": "ОбластьШапка",
            "area_occurrence": 2,
            "max_areas": 1,
        }
    )

    assert result["status"] == "ok"
    assert seen_payloads[0]["max_areas"] == 5000
    assert result["query"]["area_match"] == "exact"
    assert result["query"]["area_occurrence"] == 2
    assert result["counts"]["area_filter_applied"] is True
    assert result["counts"]["decoded_named_areas"] == 3
    assert result["counts"]["areas"] == 1
    assert result["counts"]["matching_areas"] == 1
    assert result["counts"]["areas_limited"] is False
    assert result["areas"][0]["name"] == "ОбластьШапка"
    assert result["areas"][0]["occurrence"] == 2
    assert result["areas"][0]["range"]["one_based"] == {"top": 1, "left": 1, "bottom": 3, "right": 4}


def test_templates_areas_find_rejects_zero_occurrence() -> None:
    result = adapter_server.validate_templates_areas_find_payload(
        {"base_id": "upo_test", "route_ref": "ConfigCAS:" + ("a" * 40), "area_name": "Header", "occurrence": 0}
    )

    assert result is not None
    assert result["status"] == "invalid_argument"
    assert result["argument"] == "area_occurrence"


def test_templates_areas_find_accepts_occurrence_alias(monkeypatch: pytest.MonkeyPatch) -> None:
    descriptor_key = "a" * 40

    monkeypatch.setattr(
        adapter_server,
        "read_template_by_route",
        lambda payload, analyze=False: {
            "status": "ok",
            "templates": [
                {
                    "name": "RouteRefTemplate",
                    "guid": descriptor_key,
                    "structure": {
                        "format": "MOXCEL",
                        "named_areas": [
                            {"name": "Header", "occurrence": 1, "range": {"one_based": {"top": 1, "left": 1, "bottom": 1, "right": 4}}},
                            {"name": "Header", "occurrence": 2, "range": {"one_based": {"top": 2, "left": 1, "bottom": 2, "right": 4}}},
                        ],
                    },
                }
            ],
            "counts": {"templates": 1, "parts": 1},
        },
    )

    validation = adapter_server.validate_templates_areas_find_payload(
        {"base_id": "upo_test", "route_ref": f"ConfigCAS:{descriptor_key}", "area_name": "Header", "occurrence": 2}
    )
    result = adapter_server.templates_areas_find(
        {"base_id": "upo_test", "route_ref": f"ConfigCAS:{descriptor_key}", "area_name": "Header", "occurrence": 2}
    )

    assert validation is None
    assert result["status"] == "ok"
    assert result["query"]["area_occurrence"] == 2
    assert result["counts"]["areas"] == 1
    assert result["areas"][0]["occurrence"] == 2


def test_templates_read_accepts_route_ref(monkeypatch: pytest.MonkeyPatch) -> None:
    descriptor_key = "a" * 40
    seen_payloads: list[dict[str, Any]] = []

    def fake_read_template_by_route(payload: dict[str, Any], *, analyze: bool = False) -> dict[str, Any]:
        seen_payloads.append(payload)
        return {"status": "ok", "templates": [], "counts": {"templates": 0, "parts": 0}}

    monkeypatch.setattr(adapter_server, "read_template_by_route", fake_read_template_by_route)

    validation = adapter_server.validate_templates_read_payload({"base_id": "upo_test", "route_ref": f"ConfigCAS:{descriptor_key}"})
    result = adapter_server.templates_read({"base_id": "upo_test", "route_ref": f"ConfigCAS:{descriptor_key}"})

    assert validation is None
    assert result["status"] == "ok"
    assert seen_payloads[0]["table"] == "ConfigCAS"
    assert seen_payloads[0]["file_name"] == descriptor_key


def test_semantic_cache_search_returns_candidate_only(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
    config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))

    adapter_server.semantic_document_cache_upsert(
        config,
        document_id="template_part:ConfigCAS:part-key:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
        object_kind="Template",
        object_guid="part-key",
        object_name="ТестовыйМакет",
        extension_guid="99999999-9999-9999-9999-999999999999",
        source_route={"table": "ConfigCAS", "file_name": "part-key", "part_id": "part-key"},
        content_sha1="a" * 40,
        text="area: ОбластьШапка\nparameter: Период\ncell R1C1: Меню за период",
    )

    result = adapter_server.semantic_cache_search({"base_id": "upo_test", "query": "ОбластьШапка", "kind": "Template"})

    assert result["status"] == "ok"
    assert result["source"]["authoritative"] is False
    assert result["matches"][0]["freshness"]["status"] == "candidate_only"
    assert result["matches"][0]["freshness"]["validation_required"] is True
    assert result["matches"][0]["read_selector"]["method"] == "templates.read"
    assert result["matches"][0]["cache"]["authoritative_source"] == "decoded_artifact_cache"


def test_semantic_cache_search_can_validate_top_candidates(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
    config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    data = b"validated-template-payload"
    adapter_server.semantic_document_cache_upsert(
        config,
        document_id="doc-validated",
        object_kind="Template",
        object_guid="validated-template",
        object_name="ValidatedTemplate",
        extension_guid=None,
        source_route={"table": "ConfigCAS", "file_name": "validated-template"},
        content_sha1=hashlib.sha1(data).hexdigest(),
        text="area: ОбластьШапка\ncell R1C1: Меню за период",
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda base_id, table, file_name, timeout_seconds=30: (data, None, None),
    )

    result = adapter_server.semantic_cache_search(
        {
            "base_id": "upo_test",
            "query": "ОбластьШапка",
            "kind": "Template",
            "validate_candidates": True,
        }
    )

    assert result["status"] == "ok"
    assert result["query"]["validate_candidates"] is True
    assert result["counts"]["validation"]["checked"] == 1
    assert result["counts"]["validation"]["fresh"] == 1
    assert result["matches"][0]["freshness"]["status"] == "fresh"
    assert result["matches"][0]["freshness"]["validation_required"] is False
    assert result["matches"][0]["validation"]["source"]["authoritative"] is True


def test_semantic_embedding_upsert_requires_content_precondition(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
    config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    document_id = "template_part:ConfigCAS:part-key:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

    adapter_server.semantic_document_cache_upsert(
        config,
        document_id=document_id,
        object_kind="Template",
        object_guid="part-key",
        object_name="ТестовыйМакет",
        extension_guid=None,
        source_route={"table": "ConfigCAS", "file_name": "part-key"},
        content_sha1="a" * 40,
        text="area: ОбластьШапка",
    )

    pending = adapter_server.semantic_cache_pending({"base_id": "upo_test"})
    assert pending["documents"][0]["precondition"]["content_sha1"] == "a" * 40

    stored = adapter_server.semantic_cache_embedding_upsert(
        {
            "base_id": "upo_test",
            "document_id": document_id,
            "content_sha1": "a" * 40,
            "embedding_model": "test-embedding",
            "embedding": [1.0, 0.0],
        }
    )
    assert stored["status"] == "ok"
    assert stored["dimensions"] == 2

    vector_result = adapter_server.semantic_cache_search({"base_id": "upo_test", "query_embedding": [1.0, 0.0]})
    assert vector_result["status"] == "ok"
    assert vector_result["matches"][0]["match_by"] == "vector_embedding"
    assert vector_result["matches"][0]["cache"]["embedding_model"] == "test-embedding"

    rejected = adapter_server.semantic_cache_embedding_upsert(
        {
            "base_id": "upo_test",
            "document_id": document_id,
            "content_sha1": "b" * 40,
            "embedding_model": "test-embedding",
            "embedding": [0.0, 1.0],
        }
    )
    assert rejected["status"] == "conflict"
    assert rejected["error"] == "content_sha1_mismatch"


def test_semantic_cache_status_reports_vector_readiness(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
    config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))

    adapter_server.semantic_document_cache_upsert(
        config,
        document_id="doc-pending",
        object_kind="Template",
        object_guid="pending-template",
        object_name="PendingTemplate",
        extension_guid=None,
        source_route={"table": "ConfigCAS", "file_name": "pending-template"},
        content_sha1="a" * 40,
        text="area: ОбластьШапка",
    )
    adapter_server.semantic_document_cache_upsert(
        config,
        document_id="doc-embedded",
        object_kind="Template",
        object_guid="embedded-template",
        object_name="EmbeddedTemplate",
        extension_guid=None,
        source_route={"table": "ConfigCAS", "file_name": "embedded-template"},
        content_sha1="b" * 40,
        text="area: ОбластьТело",
    )
    stored = adapter_server.semantic_cache_embedding_upsert(
        {
            "base_id": "upo_test",
            "document_id": "doc-embedded",
            "content_sha1": "b" * 40,
            "embedding_model": "test-embedding",
            "embedding": [0.0, 1.0],
        }
    )
    assert stored["status"] == "ok"

    result = adapter_server.semantic_cache_status({"base_id": "upo_test", "kind": "Template", "include_entries": True})

    assert result["status"] == "ok"
    assert result["schema"] == "onec_semantic_cache_status.v1"
    assert result["source"]["authoritative"] is False
    assert result["counts"]["total"] == 2
    assert result["counts"]["pending_embedding"] == 1
    assert result["counts"]["embedded"] == 1
    assert any(group["vector_status"] == "pending_embedding" and group["count"] == 1 for group in result["groups"])
    assert any(
        group["vector_status"] == "embedded"
        and group["embedding_model"] == "test-embedding"
        and group["count"] == 1
        for group in result["groups"]
    )
    assert {entry["document_id"] for entry in result["entries"]} == {"doc-pending", "doc-embedded"}


def test_semantic_cache_validate_confirms_current_source_sha(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
    config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    data = b"current-template-payload"
    document_id = "doc-fresh"
    adapter_server.semantic_document_cache_upsert(
        config,
        document_id=document_id,
        object_kind="Template",
        object_guid="fresh-template",
        object_name="FreshTemplate",
        extension_guid=None,
        source_route={"table": "ConfigCAS", "file_name": "fresh-template"},
        content_sha1=hashlib.sha1(data).hexdigest(),
        text="area: ОбластьШапка",
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda base_id, table, file_name, timeout_seconds=30: (data, None, None),
    )

    result = adapter_server.semantic_cache_validate({"base_id": "upo_test", "document_id": document_id})

    assert result["status"] == "ok"
    assert result["source"]["authoritative"] is True
    assert result["freshness"]["status"] == "fresh"
    assert result["read_selector"]["method"] == "templates.read"


def test_semantic_cache_validate_marks_changed_payload_stale(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
    config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    document_id = "doc-stale"
    adapter_server.semantic_document_cache_upsert(
        config,
        document_id=document_id,
        object_kind="Template",
        object_guid="stale-template",
        object_name="StaleTemplate",
        extension_guid=None,
        source_route={"table": "ConfigCAS", "file_name": "stale-template"},
        content_sha1=hashlib.sha1(b"old-template-payload").hexdigest(),
        text="area: ОбластьШапка",
    )
    stored = adapter_server.semantic_cache_embedding_upsert(
        {
            "base_id": "upo_test",
            "document_id": document_id,
            "content_sha1": hashlib.sha1(b"old-template-payload").hexdigest(),
            "embedding_model": "test-embedding",
            "embedding": [1.0, 0.0],
        }
    )
    assert stored["status"] == "ok"
    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda base_id, table, file_name, timeout_seconds=30: (b"new-template-payload", None, None),
    )

    result = adapter_server.semantic_cache_validate({"base_id": "upo_test", "document_id": document_id})
    status = adapter_server.semantic_cache_status({"base_id": "upo_test", "kind": "Template"})

    assert result["status"] == "stale"
    assert result["error"] == "content_changed"
    assert result["freshness"]["validation_required"] is True
    assert result["cache"]["cached_content_sha1"] == hashlib.sha1(b"old-template-payload").hexdigest()
    assert result["cache"]["current_content_sha1"] == hashlib.sha1(b"new-template-payload").hexdigest()
    assert status["counts"]["error"] == 1
    assert status["counts"]["embedded"] == 0


def test_semantic_cache_validate_batch_summarizes_fresh_and_stale(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
    config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    payload_by_file = {
        "fresh-template": b"fresh-template-payload",
        "stale-template": b"new-stale-template-payload",
    }
    old_stale_sha1 = hashlib.sha1(b"old-stale-template-payload").hexdigest()
    adapter_server.semantic_document_cache_upsert(
        config,
        document_id="doc-fresh",
        object_kind="Template",
        object_guid="fresh-template",
        object_name="FreshTemplate",
        extension_guid=None,
        source_route={"table": "ConfigCAS", "file_name": "fresh-template"},
        content_sha1=hashlib.sha1(payload_by_file["fresh-template"]).hexdigest(),
        text="area: ОбластьШапка",
    )
    adapter_server.semantic_document_cache_upsert(
        config,
        document_id="doc-stale",
        object_kind="Template",
        object_guid="stale-template",
        object_name="StaleTemplate",
        extension_guid=None,
        source_route={"table": "ConfigCAS", "file_name": "stale-template"},
        content_sha1=old_stale_sha1,
        text="area: ОбластьТело",
    )
    stored = adapter_server.semantic_cache_embedding_upsert(
        {
            "base_id": "upo_test",
            "document_id": "doc-stale",
            "content_sha1": old_stale_sha1,
            "embedding_model": "test-embedding",
            "embedding": [1.0, 0.0],
        }
    )
    assert stored["status"] == "ok"

    def fake_read_storage_file_bytes(base_id: str, table: str, file_name: str, timeout_seconds: int = 30) -> tuple[bytes | None, None, None]:
        return payload_by_file.get(file_name), None, None

    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)

    result = adapter_server.semantic_cache_validate_batch({"base_id": "upo_test", "kind": "Template"})
    status = adapter_server.semantic_cache_status({"base_id": "upo_test", "kind": "Template"})

    assert result["status"] == "ok"
    assert result["schema"] == "onec_semantic_cache_validate_batch.v1"
    assert result["counts"]["checked"] == 2
    assert result["counts"]["fresh"] == 1
    assert result["counts"]["stale"] == 1
    assert {item["status"] for item in result["results"]} == {"ok", "stale"}
    stale = next(item for item in result["results"] if item["status"] == "stale")
    assert stale["error"] == "content_changed"
    assert stale["cache"]["cached_content_sha1"] == old_stale_sha1
    assert status["counts"]["error"] == 1
    assert status["counts"]["embedded"] == 0


def test_semantic_cache_refresh_redecodes_changed_template_and_queues_embedding(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
    config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    old_sha1 = hashlib.sha1(b"old-template-payload").hexdigest()
    new_payload = b"new-template-payload"
    new_sha1 = hashlib.sha1(new_payload).hexdigest()
    old_document_id = f"template_part:ConfigCAS:refresh-template:{old_sha1}"
    adapter_server.semantic_document_cache_upsert(
        config,
        document_id=old_document_id,
        object_kind="Template",
        object_guid="refresh-template",
        object_name="RefreshTemplate",
        extension_guid=None,
        source_route={"table": "ConfigCAS", "file_name": "refresh-template", "part_id": "refresh-template"},
        content_sha1=old_sha1,
        text="area: СтараяОбласть",
    )
    stored = adapter_server.semantic_cache_embedding_upsert(
        {
            "base_id": "upo_test",
            "document_id": old_document_id,
            "content_sha1": old_sha1,
            "embedding_model": "test-embedding",
            "embedding": [1.0, 0.0],
        }
    )
    assert stored["status"] == "ok"
    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda base_id, table, file_name, timeout_seconds=60: (new_payload, None, None),
    )
    monkeypatch.setattr(
        adapter_server,
        "extract_moxel_public_structure",
        lambda data: {
            "format": "MOXCEL",
            "dimensions": {"rows": 2, "columns": 2},
            "named_areas": [{"name": "ОбластьШапка", "range": {"address": "R1C1:R1C2"}}],
            "parameters": [{"name": "Период"}],
            "cells": [{"row": 1, "column": 1, "text": "Меню за период"}],
            "diagnostics": [],
        },
    )

    result = adapter_server.semantic_cache_refresh({"base_id": "upo_test", "document_id": old_document_id})
    status = adapter_server.semantic_cache_status({"base_id": "upo_test", "kind": "Template", "include_entries": True})
    pending = adapter_server.semantic_cache_pending({"base_id": "upo_test", "kind": "Template"})

    assert result["status"] == "ok"
    assert result["previous_document_id"] == old_document_id
    assert result["document_id"] == f"template_part:ConfigCAS:refresh-template:{new_sha1}"
    assert result["cache"]["content_changed"] is True
    assert result["cache"]["vector_status"] == "pending_embedding"
    assert result["precondition"]["content_sha1"] == new_sha1
    assert status["counts"]["total"] == 1
    assert status["counts"]["pending_embedding"] == 1
    assert status["counts"]["embedded"] == 0
    assert status["entries"][0]["document_id"] == result["document_id"]
    assert pending["documents"][0]["document_id"] == result["document_id"]
    assert pending["documents"][0]["precondition"]["content_sha1"] == new_sha1


def test_semantic_cache_rebuild_warms_from_extension_route_cache(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
    config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
    descriptor_key = "a" * 40
    part_key = "b" * 40
    content_sha1 = "c" * 40
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    monkeypatch.setattr(adapter_server, "extension_filter_to_guid", lambda base_id, value, method: (None, None))
    adapter_server.extension_route_cache_upsert(
        config,
        {
            "kind": "Template",
            "kind_ru": "Макет",
            "name": "RouteTemplate",
            "guid": descriptor_key,
            "origin": {"extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"}},
            "route": {"route_type": "extension_manifest_cas", "table": "ConfigCAS", "file_name": descriptor_key},
            "manifest_entries": [],
        },
        descriptor_payload_sha1="sha",
    )
    calls: list[dict[str, Any]] = []

    def fake_read_template_by_route(payload: dict[str, Any], *, analyze: bool = False) -> dict[str, Any]:
        calls.append(payload)
        adapter_server.semantic_document_cache_upsert(
            config,
            document_id=f"template_part:ConfigCAS:{part_key}:{content_sha1}",
            object_kind="Template",
            object_guid=part_key,
            object_name="RouteTemplate",
            extension_guid=None,
            source_route={"table": "ConfigCAS", "file_name": part_key, "part_id": part_key},
            content_sha1=content_sha1,
            text="area: ОбластьШапка\ncell R1C1: Меню за период",
        )
        return {"status": "ok", "counts": {"templates": 1, "parts": 1}}

    monkeypatch.setattr(adapter_server, "read_template_by_route", fake_read_template_by_route)

    result = adapter_server.semantic_cache_rebuild({"base_id": "upo_test", "kind": "Template", "include_entries": True})
    pending = adapter_server.semantic_cache_pending({"base_id": "upo_test", "kind": "Template"})

    assert result["status"] == "ok"
    assert result["source"]["authoritative"] is False
    assert result["counts"]["routes_scanned"] == 1
    assert result["counts"]["templates_read"] == 1
    assert result["counts"]["parts_seen"] == 1
    assert result["counts"]["semantic_documents_created"] == 1
    assert result["counts"]["errors"] == 0
    assert result["entries"][0]["descriptor_cas_key"] == descriptor_key
    assert calls[0]["file_name"] == descriptor_key
    assert pending["documents"][0]["document_id"] == f"template_part:ConfigCAS:{part_key}:{content_sha1}"


def test_modules_search_does_not_pass_empty_prefix_to_storage_list(monkeypatch: pytest.MonkeyPatch) -> None:
    seen_payloads: list[dict] = []

    def fake_storage_files_list(payload: dict) -> dict:
        seen_payloads.append(payload)
        assert "prefix" not in payload
        return {"status": "ok", "files": [], "counts": {"files": 0}}

    monkeypatch.setattr(adapter_server, "storage_files_list", fake_storage_files_list)
    monkeypatch.setattr(adapter_server, "read_storage_files_bytes", lambda *args, **kwargs: ({}, {}, None))

    result = adapter_server.search_modules({"base_id": "upo_test", "query": "НетТакогоТекста", "scan_limit": 10})

    assert result["status"] == "ok"
    assert result["counts"]["matches"] == 0
    assert seen_payloads


def test_metadata_kinds_include_report_and_data_processor_roles(monkeypatch: pytest.MonkeyPatch) -> None:
    class Record:
        def __init__(self, guid: str, storage_role: str) -> None:
            self.guid = guid
            self.storage_role = storage_role

    records = [
        Record("11111111-1111-1111-1111-111111111111", "Report"),
        Record("22222222-2222-2222-2222-222222222222", "DataProcessor"),
    ]
    monkeypatch.setattr(adapter_server, "live_dbnames_records", lambda *args, **kwargs: (records, None))

    result = adapter_server.get_kinds("upo_test")

    assert result["status"] == "ok"
    assert result["internal_kind_counts"]["Report"] == 1
    assert result["internal_kind_counts"]["DataProcessor"] == 1
    assert {"kind": "report", "count": 1} in result["kinds"]
    assert {"kind": "processing", "count": 1} in result["kinds"]


def test_get_object_reads_extension_data_processor_from_configcas(monkeypatch: pytest.MonkeyPatch) -> None:
    class Record:
        guid = "22222222-2222-2222-2222-222222222222"
        storage_role = "DataProcessor"
        source = "DBNames-Ext-11111111-1111-1111-1111-111111111111"
        sql_number = 1
        index = 1

    read_tables: list[str] = []
    monkeypatch.setattr(adapter_server, "live_dbnames_records", lambda *args, **kwargs: ([Record()], None))
    monkeypatch.setattr(adapter_server, "metadata_cache_lookup_row", lambda *args, **kwargs: None)

    def fake_read_files(base_id: str, table: str, file_names: list[str], **kwargs: Any) -> tuple[dict[str, bytes], dict[str, str], None]:
        read_tables.append(table)
        if table == "ConfigCAS":
            return {Record.guid: b"payload"}, {"database": "upo_test"}, None
        return {}, {"database": "upo_test"}, None

    monkeypatch.setattr(adapter_server, "read_storage_files_bytes", fake_read_files)
    monkeypatch.setattr(
        adapter_server,
        "config_identity_from_bytes",
        lambda data: {"guid": Record.guid, "name": "ТестоваяОбработка", "synonyms": {"ru": "Тестовая обработка"}} if data else None,
    )

    result = adapter_server.get_object(
        "Обработки",
        "ТестоваяОбработка",
        base_id="upo_test",
        table="ConfigCAS",
        include_semantic=False,
        include_storage=True,
    )

    assert result["status"] == "ok"
    assert result["object"]["kind"] == "DataProcessor"
    assert result["object"]["source"] == "extension"
    assert result["object"]["storage"]["table"] == "ConfigCAS"
    assert read_tables == ["ConfigCAS"]


def test_get_object_prefers_exact_name_over_cached_synonym(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"database": "upo_test"}, None))
    monkeypatch.setattr(adapter_server, "fast_extension_object_exact_match", lambda *args, **kwargs: None)
    monkeypatch.setattr(
        adapter_server,
        "metadata_cache_lookup_row",
        lambda *args, **kwargs: {
            "guid": "old-guid", "kind": "CommonModule", "name": "УОП_ЦенообразованиеСервер",
            "synonym": "Ценообразование сервер", "source": "base",
        },
    )
    seen: dict[str, Any] = {}

    def fake_list_objects(kind: str, **kwargs: Any) -> dict[str, Any]:
        seen.update(kwargs)
        return {"status": "ok", "objects": [{
            "guid": "exact-guid", "kind": "CommonModule", "name": "ЦенообразованиеСервер",
            "synonym": "Ценообразование сервер", "source": "base",
            "ref": "CommonModule.ЦенообразованиеСервер",
        }]}

    monkeypatch.setattr(adapter_server, "list_objects", fake_list_objects)

    result = adapter_server.get_object("CommonModule", "ЦенообразованиеСервер", base_id="upo_test", include_semantic=False)

    assert result["status"] == "ok"
    assert result["object"]["guid"] == "exact-guid"
    assert result["object"]["name"] == "ЦенообразованиеСервер"
    assert seen["refresh_cache"] is True
    assert seen["name_filter"] == "ЦенообразованиеСервер"


def test_get_object_falls_back_to_extension_definition_for_data_processor(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "live_dbnames_records", lambda *args, **kwargs: ([], None))
    monkeypatch.setattr(adapter_server, "metadata_cache_lookup_row", lambda *args, **kwargs: None)
    monkeypatch.setattr(
        adapter_server,
        "metadata_extension_definition_matches",
        lambda **kwargs: (
            [
                {
                    "kind": "Определение расширения",
                    "guid": "22222222-2222-2222-2222-222222222222",
                    "name": "ТестоваяОбработка",
                    "synonym": "Тестовая обработка",
                    "source": {"table": "ConfigCAS", "file_name": "22222222-2222-2222-2222-222222222222"},
                }
            ],
            [],
        ),
    )

    result = adapter_server.get_object(
        "Обработки",
        "ТестоваяОбработка",
        base_id="upo_test",
        table="ConfigCAS",
        include_semantic=False,
        include_storage=True,
    )

    assert result["status"] == "ok"
    assert result["object"]["kind"] == "DataProcessor"
    assert result["object"]["source"] == "extension"
    assert result["object"]["storage"]["table"] == "ConfigCAS"
    assert result["object"]["match_by"] == "extension_definition"


def test_extension_definition_match_accepts_configcas_identity_without_dbnames_source(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "live_dbnames_records", lambda *args, **kwargs: ([], None))
    monkeypatch.setattr(adapter_server, "extension_map_by_guid", lambda *args, **kwargs: {})
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda *args, **kwargs: (None, None))
    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {"status": "ok", "files": [{"FileName": "cas-file"}]},
    )
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (b"payload", {}, None))
    monkeypatch.setattr(adapter_server, "read_storage_files_bytes", lambda *args, **kwargs: ({"cas-file": b"payload"}, {}, None))
    monkeypatch.setattr(
        adapter_server,
        "payload_text_from_bytes",
        lambda data: {"text": "935ca39d-dfc4-445d-b696-03484dc5b145"},
    )
    monkeypatch.setattr(
        adapter_server,
        "config_identity_from_bytes",
        lambda data: {"guid": "935ca39d-dfc4-445d-b696-03484dc5b145", "name": "ТестоваяОбработка", "synonyms": {}},
    )

    matches, diagnostics = adapter_server.metadata_extension_definition_matches(
        base_id="upo_test",
        query="ТестоваяОбработка",
        max_files=10,
        max_matches=5,
        timeout_seconds=30,
        include_storage=True,
    )

    assert len(matches) == 1
    assert matches[0]["name"] == "ТестоваяОбработка"
    assert matches[0]["kind"] == "Определение расширения"
    assert matches[0]["source"]["table"] == "ConfigCAS"
    assert matches[0]["source"]["file_name"] == "cas-file"
    assert any(item.get("scanned_payloads") == 1 for item in diagnostics)


def test_dynamic_list_settings_detect_query_text_and_flags() -> None:
    node = {
        "type": "list",
        "items": [
            {"type": "string", "value": "ОсновнаяТаблица"},
            {"type": "string", "value": "Catalog.Номенклатура"},
            {"type": "string", "value": "ПроизвольныйЗапрос"},
            {"type": "string", "value": "B"},
            {"type": "number", "value": "1"},
            {"type": "string", "value": "ТекстЗапроса"},
            {"type": "string", "value": "S"},
            {"type": "string", "value": "ВЫБРАТЬ Номенклатура.Ссылка ИЗ Справочник.Номенклатура КАК Номенклатура"},
        ],
    }

    settings = dynamic_list_settings(node, owner_path="3.4")

    assert settings == {
        "main_table": "Catalog.Номенклатура",
        "custom_query": True,
        "query_text": "ВЫБРАТЬ Номенклатура.Ссылка ИЗ Справочник.Номенклатура КАК Номенклатура",
        "paths": {"main_table": "3.4.1", "custom_query": "3.4.4", "query_text": "3.4.7"},
        "sources": {"main_table": "key_value", "custom_query": "key_value", "query_text": "key_value"},
        "status": "ok",
    }


def test_definition_metadata_match_accepts_public_kind_reference(monkeypatch: pytest.MonkeyPatch) -> None:
    scanned_kinds: list[str] = []
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"server": "sql-host", "database": "upo_test"}, None))

    def fake_list_objects(kind: str, **kwargs: Any) -> dict[str, Any]:
        scanned_kinds.append(kind)
        assert kind == "DataProcessor"
        return {
            "status": "ok",
            "objects": [
                {
                    "kind": "DataProcessor",
                    "kind_ru": "Обработка",
                    "public_kind": "processing",
                    "name": "ТестоваяОбработка",
                    "synonym": "Тестовая обработка",
                    "guid": "22222222-2222-2222-2222-222222222222",
                    "source": "base",
                }
            ],
        }

    monkeypatch.setattr(adapter_server, "list_objects", fake_list_objects)

    matches, diagnostics = adapter_server.metadata_configuration_definition_matches(
        base_id="upo_test",
        query="Обработка.ТестоваяОбработка",
        max_matches=5,
        use_cache=False,
    )

    assert scanned_kinds == ["DataProcessor"]
    assert diagnostics == [{"area": "metadata", "source": "live_metadata", "cache": "not_used", "matches": 1}]
    assert len(matches) == 1
    assert matches[0]["kind"] == "Обработка"
    assert matches[0]["name"] == "ТестоваяОбработка"
    assert matches[0]["read_selector"]["kind"] == "DataProcessor"
    assert matches[0]["read_selector"]["ref"] == "DataProcessor.ТестоваяОбработка"
    assert matches[0]["related_selectors"]["modules"]["method"] == "metadata.object.modules"
    assert matches[0]["related_selectors"]["code_search"]["method"] == "code.search"
    assert matches[0]["related_selectors"]["modules_search"]["method"] == "modules.search"
    assert matches[0]["related_selectors"]["card"]["method"] == "metadata.object.get"
    assert matches[0]["related_selectors"]["full"]["method"] == "metadata.object.full"
    assert "query" not in matches[0]["related_selectors"]["code_search"]
    assert matches[0]["related_selectors"]["forms"]["kind"] == "DataProcessor"
    assert matches[0]["related_selectors"]["forms"]["ref"] == "DataProcessor.ТестоваяОбработка"


def test_definition_metadata_live_match_does_not_require_cache_config(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "sql_config_for_base",
        lambda base_id: pytest.fail("live metadata definition lookup must not require cache config"),
    )
    monkeypatch.setattr(
        adapter_server,
        "list_objects",
        lambda kind, **kwargs: {
            "status": "ok",
            "objects": [
                {
                    "kind": "DataProcessor",
                    "kind_ru": "Обработка",
                    "public_kind": "processing",
                    "name": "ТестоваяОбработка",
                    "synonym": "Тестовая обработка",
                    "guid": "22222222-2222-2222-2222-222222222222",
                    "source": "base",
                }
            ],
        },
    )

    matches, diagnostics = adapter_server.metadata_configuration_definition_matches(
        base_id="upo_test",
        query="Обработка.ТестоваяОбработка",
        max_matches=5,
        use_cache=False,
    )

    assert len(matches) == 1
    assert matches[0]["name"] == "ТестоваяОбработка"
    assert diagnostics == [{"area": "metadata", "source": "live_metadata", "cache": "not_used", "matches": 1}]


def test_definition_find_promotes_single_metadata_object_and_selectors(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"server": "sql-host", "database": "upo_test"}, None))
    monkeypatch.setattr(
        adapter_server,
        "list_objects",
        lambda kind, **kwargs: {
            "status": "ok",
            "objects": [
                {
                    "kind": "DataProcessor",
                    "kind_ru": "Обработка",
                    "public_kind": "processing",
                    "name": "ТестоваяОбработка",
                    "synonym": "Тестовая обработка",
                    "guid": "22222222-2222-2222-2222-222222222222",
                    "source": "base",
                }
            ],
        },
    )

    result = adapter_server.metadata_definition_find(
        {
            "base_id": "upo_test",
            "query": "Обработка.ТестоваяОбработка",
            "areas": ["metadata"],
            "max_matches": 5,
        }
    )

    assert result["status"] == "ok"
    assert result["object"]["kind"] == "DataProcessor"
    assert result["object"]["name"] == "ТестоваяОбработка"
    assert result["related_selectors"]["modules"]["guid"] == "22222222-2222-2222-2222-222222222222"
    assert result["related_selectors"]["modules"]["ref"] == "DataProcessor.ТестоваяОбработка"
    assert result["related_selectors"]["form_details"]["method"] == "metadata.object.form.details"
    assert result["related_selectors"]["modules_search"]["method"] == "modules.search"
    assert result["related_selectors"]["card"]["method"] == "metadata.object.get"


def test_extension_object_read_selectors_include_form_routes_for_form_capable_kind() -> None:
    selectors = adapter_server.extension_object_read_selectors(
        "upo_test",
        "DataProcessor",
        {"name": "AnyProcessing", "guid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"},
        {"table": "ConfigCAS", "file_name": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"},
    )

    assert selectors["forms"] == {
        "method": "metadata.object.forms",
        "base_id": "upo_test",
        "kind": "DataProcessor",
        "guid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
        "name": "AnyProcessing",
        "table": "ConfigCAS",
    }
    assert selectors["form_details"]["method"] == "metadata.object.form.details"
    assert selectors["form_details"]["guid"] == "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
    assert selectors["form_details"]["table"] == "ConfigCAS"


def test_definition_find_promotes_resolved_guid_selector_without_metadata_match(monkeypatch: pytest.MonkeyPatch) -> None:
    guid = "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec"

    monkeypatch.setattr(
        adapter_server,
        "get_object",
        lambda *args, **kwargs: {
            "status": "ok",
            "object": {
                "kind": "Document",
                "kind_ru": "Документ",
                "name": "АвансовыйОтчет",
                "synonym": "Авансовый отчет",
                "guid": guid,
            },
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_configuration_definition_matches",
        lambda **kwargs: ([], [{"area": "metadata", "source": "live_metadata", "matches": 0}]),
    )

    result = adapter_server.metadata_definition_find(
        {
            "base_id": "upo_test",
            "query": guid,
            "guid": guid,
            "areas": ["metadata"],
            "exact_only": True,
            "max_matches": 5,
        }
    )

    assert result["status"] == "ok"
    assert result["object"]["guid"] == guid
    assert result["matches"][0]["area"] == "metadata"
    assert result["matches"][0]["match_by"] == "guid_exact"
    assert result["related_selectors"]["modules"]["method"] == "metadata.object.modules"


def test_definition_find_accepts_object_type_name_aliases(monkeypatch: pytest.MonkeyPatch) -> None:
    seen_get_object: dict[str, Any] = {}
    seen_attributes: list[dict[str, Any]] = []

    def fake_get_object(kind: str | None, name_or_guid: str, **kwargs: Any) -> dict[str, Any]:
        seen_get_object.update({"kind": kind, "name_or_guid": name_or_guid})
        return {
            "status": "ok",
            "object": {
                "kind": "DataProcessor",
                "kind_ru": "Обработка",
                "public_kind": "processing",
                "name": "ТестоваяОбработка",
                "synonym": "Тестовая обработка",
                "guid": "22222222-2222-2222-2222-222222222222",
            },
        }

    def fake_metadata_object_attributes(payload: dict[str, Any]) -> dict[str, Any]:
        seen_attributes.append(payload)
        return {
            "status": "ok",
            "dimensions": [],
            "resources": [],
            "attributes": [{"name": "ЦелевойРеквизит", "type": {"presentation": "Строка"}}],
            "tabular_sections": [],
        }

    monkeypatch.setattr(adapter_server, "get_object", fake_get_object)
    monkeypatch.setattr(adapter_server, "metadata_object_attributes", fake_metadata_object_attributes)

    result = adapter_server.call_method(
        "metadata.definition.find",
        {
            "base_id": "upo_test",
            "object_type": "Обработка",
            "object_name": "ТестоваяОбработка",
            "query": "ЦелевойРеквизит",
            "areas": ["object"],
        },
    )

    assert result["status"] == "ok"
    assert seen_get_object == {"kind": "DataProcessor", "name_or_guid": "ТестоваяОбработка"}
    assert seen_attributes[0]["kind"] == "DataProcessor"
    assert seen_attributes[0]["name"] == "ТестоваяОбработка"
    assert result["matches"][0]["object"]["kind"] == "DataProcessor"


def test_definition_find_direct_call_accepts_object_type_name_aliases(monkeypatch: pytest.MonkeyPatch) -> None:
    seen_get_object: dict[str, Any] = {}

    def fake_get_object(kind: str | None, name_or_guid: str, **kwargs: Any) -> dict[str, Any]:
        seen_get_object.update({"kind": kind, "name_or_guid": name_or_guid})
        return {
            "status": "ok",
            "object": {
                "kind": "DataProcessor",
                "kind_ru": "Обработка",
                "public_kind": "processing",
                "name": "ТестоваяОбработка",
                "guid": "22222222-2222-2222-2222-222222222222",
            },
        }

    monkeypatch.setattr(adapter_server, "get_object", fake_get_object)
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_attributes",
        lambda payload: {
            "status": "ok",
            "dimensions": [],
            "resources": [],
            "attributes": [{"name": "ЦелевойРеквизит"}],
            "tabular_sections": [],
        },
    )

    result = adapter_server.metadata_definition_find(
        {
            "base_id": "upo_test",
            "object_type": "Обработка",
            "object_name": "ТестоваяОбработка",
            "query": "ЦелевойРеквизит",
            "areas": ["object"],
        }
    )

    assert result["status"] == "ok"
    assert seen_get_object == {"kind": "DataProcessor", "name_or_guid": "ТестоваяОбработка"}


def test_definition_find_fast_report_field_uses_scd_lineage_before_broad_search(monkeypatch: pytest.MonkeyPatch) -> None:
    seen_lineage_payload: dict[str, Any] = {}
    monkeypatch.setattr(
        adapter_server,
        "get_object",
        lambda *args, **kwargs: {
            "status": "ok",
            "object": {
                "kind": "Report",
                "kind_ru": "Отчет",
                "name": "Продажи",
                "guid": "33333333-3333-3333-3333-333333333333",
            },
        },
    )
    def fake_dcs_field_lineage(payload: dict[str, Any]) -> dict[str, Any]:
        seen_lineage_payload.update(payload)
        return {
            "status": "ok",
            "lineage": [{"dataset": "НаборДанных1", "field": "Цена", "expression": "Продажи.Цена"}],
            "agent_summary": {"status": "ok", "confirmed_sources": [{"source": "РегистрСведений.Цены.Цена"}]},
        }

    monkeypatch.setattr(adapter_server, "dcs_field_lineage", fake_dcs_field_lineage)
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_attributes",
        lambda payload: pytest.fail("fast SCD route must not start broad object search"),
    )

    result = adapter_server.metadata_definition_find(
        {
            "base_id": "upo_test",
            "ref": "Отчет.Продажи",
            "query": "Цена",
            "search_profile": "fast",
            "template": "ОсновнаяСхемаКомпоновкиДанных",
        }
    )

    assert result["status"] == "ok"
    assert result["query"]["areas"] == ["scd"]
    assert result["matches"][0]["match_by"] == "scd_field_static"
    assert result["matches"][0]["confirmed_sources"] == [{"source": "РегистрСведений.Цены.Цена"}]
    assert seen_lineage_payload["schema"] == "ОсновнаяСхемаКомпоновкиДанных"
    assert result["matches"][0]["read_selector"]["schema"] == "ОсновнаяСхемаКомпоновкиДанных"
    assert result["next_calls"][0]["params"]["search_profile"] == "full"


def test_definition_find_accepts_explicit_scd_area(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "get_object",
        lambda *args, **kwargs: {"status": "ok", "object": {"kind": "Report", "name": "Продажи", "guid": "33333333-3333-3333-3333-333333333333"}},
    )
    monkeypatch.setattr(
        adapter_server,
        "dcs_field_lineage",
        lambda payload: {"status": "not_found", "error": "field_not_found", "diagnostics": {"code": "SCD_FIELD_NOT_FOUND"}},
    )

    result = adapter_server.metadata_definition_find(
        {"base_id": "upo_test", "ref": "Отчет.Продажи", "query": "Цена", "areas": ["схема компоновки данных"]}
    )

    assert result["status"] == "not_found"
    assert result["error"] == "field_not_found"
    assert result["query"]["areas"] == ["scd"]
    assert result["diagnostics"][0]["area"] == "scd"


def test_metadata_adapter_audit_reports_recognized_missing_and_unmapped(monkeypatch: pytest.MonkeyPatch) -> None:
    class Record:
        def __init__(self, guid: str, storage_role: str) -> None:
            self.guid = guid
            self.storage_role = storage_role

    records = [
        Record("11111111-1111-1111-1111-111111111111", "Document"),
        Record("22222222-2222-2222-2222-222222222222", "DataProcessor"),
        Record("33333333-3333-3333-3333-333333333333", "ExtDataSrcPrms"),
    ]
    monkeypatch.setattr(adapter_server, "live_dbnames_records", lambda *args, **kwargs: (records, None))

    compact = adapter_server.metadata_adapter_audit({"base_id": "upo_test"})
    detailed = adapter_server.metadata_adapter_audit({"base_id": "upo_test", "include_missing": True, "include_unmapped": True})

    assert compact["status"] == "ok"
    assert compact["counts"]["recognized_kinds"] == 2
    assert compact["recognized_kind_counts"] == {"DataProcessor": 1, "Document": 1}
    assert compact["public_kind_counts"]["document"] == 1
    assert compact["public_kind_counts"]["processing"] == 1
    assert all(item["count"] > 0 for item in compact["metadata_kinds"])
    assert compact["code_carriers"]["object_modules"]["module_roles"] == ["object_module", "manager_module", "command_module"]
    assert compact["code_carriers"]["register_modules"]["module_roles"] == ["record_set_module", "register_module", "command_module"]
    assert "CalculationRegister" in compact["code_carriers"]["register_modules"]["owner_kinds"]
    assert compact["code_carriers"]["form_modules"]["module_roles"] == ["form_module"]
    assert compact["code_carriers"]["scheduled_jobs"]["status"] == "supported_reference"
    assert compact["code_carriers"]["web_http_service_modules"]["status"] == "supported_read"
    assert compact["write_capabilities"]["bsl_modules"]["status"] == "supported_saved_state"
    assert compact["write_capabilities"]["templates"]["status"] == "read_only"

    assert detailed["counts"]["recognized_kinds"] == 2
    assert detailed["counts"]["supported_kinds"] == len(adapter_server.TOP_LEVEL_METADATA_KINDS)
    assert detailed["counts"]["missing_supported_kinds"] == len(adapter_server.TOP_LEVEL_METADATA_KINDS) - 2
    assert any(
        item["kind"] == "Report"
        and item["presence_status"] == "supported_absent_in_selected_base"
        for item in detailed["missing_supported_kinds"]
    )
    assert detailed["metadata_candidates"] == []
    assert detailed["unmapped_source_roles"] == [
        {"source_role": "ExtDataSrcPrms", "category": "auxiliary_storage", "records": 1}
    ]


def test_dbnames_platform_service_tables_are_not_reported_as_metadata_candidates() -> None:
    system_roles = {
        "Acoustic",
        "Bots",
        "Ecs",
        "LangModel",
        "MobileClientDataExchange",
        "STTGrammar",
        "STTGrammarChecksum",
        "STTModels",
        "STTModelsDesc",
        "STTSettings",
        "URLExternalData",
        "WebSocketClients",
    }

    assert {adapter_server.classify_unmapped_source_role(role) for role in system_roles} == {
        "platform_system_storage"
    }
    assert adapter_server.classify_unmapped_source_role("ExtDataSrcPrms") == "auxiliary_storage"


def test_modules_search_returns_public_read_selector_without_storage(monkeypatch: pytest.MonkeyPatch) -> None:
    class FakeStreamClassifier:
        pass

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {"status": "ok", "files": [{"FileName": "module-guid"}], "counts": {"files": 1}},
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_files_bytes",
        lambda *args, **kwargs: ({"module-guid": b"payload"}, {"database": "upo_test"}, None),
    )
    monkeypatch.setattr(adapter_server, "payload_text_from_bytes", lambda data: {"status": "ok", "text": ""})
    monkeypatch.setattr(adapter_server, "extract_bsl_text_from_container", lambda text, **kwargs: ("", {"status": "not_found"}))
    monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
    FakeStreamClassifier.classify_payload = staticmethod(
        lambda data, include_text=True: {
            "role": "bsl_module_payload",
            "stream_blocks": [{"text": "Процедура Команда()\nЗапрос = Новый Запрос;\nКонецПроцедуры\n"}],
        }
    )

    result = adapter_server.search_modules({"base_id": "upo_test", "query": "Новый Запрос", "scan_limit": 10, "state": "active"})

    assert result["status"] == "ok"
    assert result["counts"]["matches"] == 1
    assert result["counts"]["complete"] is True
    assert result["counts"]["scan_limit_hit"] is False
    assert result["counts"]["owner_unresolved"] == 1
    match = result["matches"][0]
    assert match["owner"]["status"] == "unresolved"
    assert match["read_selector"]["base_id"] == "upo_test"
    assert match["read_selector"]["method"] == "modules.read"
    assert match["read_selector"]["module_ref"] == "ConfigCAS:module-guid#stream:0"
    assert match["read_selector"]["preview"] is True
    assert "module_id" not in match
    assert "table" not in match
    assert "file_name" not in match


def test_modules_search_defaults_to_working_saved_state(monkeypatch: pytest.MonkeyPatch) -> None:
    class FakeStreamClassifier:
        pass

    seen_tables: list[str] = []
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: seen_tables.append(str(payload.get("table"))) or {"status": "ok", "files": [{"FileName": "module-guid"}], "counts": {"files": 1}},
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_files_bytes",
        lambda *args, **kwargs: ({"module-guid": b"payload"}, {"database": "upo_test"}, None),
    )
    monkeypatch.setattr(adapter_server, "payload_text_from_bytes", lambda data: {"status": "ok", "text": ""})
    monkeypatch.setattr(adapter_server, "extract_bsl_text_from_container", lambda text, **kwargs: ("", {"status": "not_found"}))
    monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
    FakeStreamClassifier.classify_payload = staticmethod(
        lambda data, include_text=True: {
            "role": "bsl_module_payload",
            "stream_blocks": [{"text": "Процедура SavedStateProbe()\nКонецПроцедуры\n"}],
        }
    )

    result = adapter_server.search_modules({"base_id": "upo_test", "query": "SavedStateProbe", "scan_limit": 10})

    assert result["status"] == "ok"
    assert result["query"]["state"] == "working"
    assert seen_tables[0] == "ConfigCASSave"
    assert result["matches"][0]["read_selector"]["module_ref"] == "ConfigCASSave:module-guid#stream:0"
    assert result["matches"][0]["activation_state"] == "saved_state"


def test_modules_search_repairs_saved_state_stream_and_reports_owner_role(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "extension-guid"
    catalog_guid = "catalog-guid"
    file_name = f"{extension_guid}__{catalog_guid}.3"
    descriptor_file_name = f"{extension_guid}__{catalog_guid}"
    good_text = "// Это модуль менеджера\r\nПроцедура ОбработкаКоманды(Команда)\r\nКонецПроцедуры\r\n"
    mojibake_text = good_text.encode("utf-8").decode("cp1251")
    module_text = mojibake_text.encode("utf-8-sig")
    header = f"\r\n{len(module_text):08x} {len(module_text):08x} 7fffffff \r\n".encode("ascii")
    stored_module = compress_payload(b"prefix" + header + module_text, "raw_deflate")
    stored_descriptor = compress_payload('{"tt_Справочник1"}'.encode("utf-8-sig"), "raw_deflate")

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {"status": "ok", "files": [{"FileName": file_name}], "counts": {"files": 1}},
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_files_bytes",
        lambda *args, **kwargs: ({file_name: stored_module}, {"database": "upo_test"}, None),
    )

    def fake_read_storage_file_bytes(base_id: str, table: str, requested_file_name: str, *, timeout_seconds: int = 30):
        assert base_id == "upo_test"
        assert table == "ConfigCASSave"
        if requested_file_name == descriptor_file_name:
            return stored_descriptor, {"database": "upo_test"}, None
        return stored_module, {"database": "upo_test"}, None

    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)

    result = adapter_server.search_modules(
        {
            "base_id": "upo_test",
            "query": "Это модуль менеджера",
            "scan_limit": 10,
            "state": "save",
            "scope": "modules",
        }
    )

    assert result["status"] == "ok"
    assert result["counts"]["matches"] == 1
    match = result["matches"][0]
    assert match["owner"]["status"] == "unresolved"
    assert match["module"]["kind"] != "manager_module"
    assert match["activation_state"] == "saved_state"
    assert match["read_selector"]["module_ref"] == f"ConfigCASSave:{file_name}#stream:0"


def test_modules_search_saved_state_form_container_reports_form_module(monkeypatch: pytest.MonkeyPatch) -> None:
    file_name = "extension-guid__form-guid.0"
    form_text = '{"meta","data","&НаКлиенте\r\nПроцедура ЗаменаДомена(Команда)\r\nКонецПроцедуры\r\n"}'

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {"status": "ok", "files": [{"FileName": file_name}], "counts": {"files": 1}},
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_files_bytes",
        lambda *args, **kwargs: ({file_name: b"payload"}, {"database": "upo_test"}, None),
    )
    monkeypatch.setattr(adapter_server, "payload_text_from_bytes", lambda data: {"status": "ok", "text": form_text})
    monkeypatch.setattr(
        adapter_server,
        "saved_state_descriptor_identity",
        lambda **kwargs: {"name": "t_Форма", "synonym": "T форма", "guid": "form-guid", "source": "saved_state_descriptor"},
    )
    monkeypatch.setattr(adapter_server, "saved_state_related_descriptor_identity", lambda **kwargs: None)

    result = adapter_server.search_modules(
        {
            "base_id": "upo_test",
            "query": "ЗаменаДомена",
            "scan_limit": 10,
            "state": "save",
            "scope": "modules",
        }
    )

    assert result["status"] == "ok"
    match = result["matches"][0]
    assert match["form"]["name"] == "t_Форма"
    assert match["module"]["kind"] == "form_module"
    assert match["module"]["name"] == "Модуль формы"
    assert match["module"]["form"] == "t_Форма"
    assert match["read_selector"]["module_ref"] == f"ConfigCASSave:{file_name}"


def test_module_roles_depend_on_owner_kind_and_suffix() -> None:
    assert adapter_server.public_module_role(owner_kind="Catalog", suffix="0") == {
        "kind": "object_module",
        "name": "Модуль объекта",
        "suffix": "0",
    }
    assert adapter_server.public_module_role(owner_kind="Document", suffix="3") == {
        "kind": "manager_module",
        "name": "Модуль менеджера",
        "suffix": "3",
    }
    assert adapter_server.public_module_role(owner_kind="InformationRegister", suffix="0") == {
        "kind": "record_set_module",
        "name": "Модуль набора записей",
        "suffix": "0",
    }
    assert adapter_server.public_module_role(owner_kind="AccumulationRegister", suffix="0") == {
        "kind": "record_set_module",
        "name": "Модуль набора записей",
        "suffix": "0",
    }
    assert adapter_server.public_module_role(owner_kind="Constant", suffix="0") == {
        "kind": "value_manager_module",
        "name": "Модуль менеджера значения",
        "suffix": "0",
    }
    assert adapter_server.public_module_role(owner_kind="Catalog", suffix="2") == {
        "kind": "command_module",
        "name": "Модуль команды",
        "suffix": "2",
    }
    assert adapter_server.public_module_role(owner_kind="Document", suffix=".2") == {
        "kind": "command_module",
        "name": "Модуль команды",
        "suffix": "2",
    }
    assert adapter_server.public_module_role(owner_kind="AccountingRegister", suffix=".6", ordinal=2) == {
        "kind": "register_module",
        "name": "Модуль регистра 2",
    }


def test_public_module_row_uses_owner_kind_for_register_module() -> None:
    module = {"module_id": "ConfigCAS:register-guid.0#stream:0", "suffix": "0"}

    public = adapter_server.public_module_row(module, include_storage=False, ordinal=1, owner_kind="InformationRegister")

    assert public["kind"] == "record_set_module"
    assert public["name"] == "Модуль набора записей"
    assert public["module_ordinal"] == 1
    assert "module_id" not in public


def test_public_module_row_keeps_public_name_with_storage_details() -> None:
    module = {
        "module_id": "Config:register-guid.6#stream:4",
        "name": "register-guid.6#stream:4",
    }

    public = adapter_server.public_module_row(module, include_storage=True, ordinal=2, owner_kind="AccountingRegister")

    assert public["kind"] == "register_module"
    assert public["name"] == "Модуль регистра 2"
    assert public["storage_name"] == "register-guid.6#stream:4"
    assert public["module_id"] == "Config:register-guid.6#stream:4"


def test_public_common_module_uses_1c_owner_name_not_storage_route() -> None:
    module = {
        "module_id": "Config:0422fa60-6cd4-410c-b2af-8792a6a9b199.0#stream:4",
        "name": "0422fa60-6cd4-410c-b2af-8792a6a9b199.0#stream:4",
        "storage_name": "0422fa60-6cd4-410c-b2af-8792a6a9b199.0#stream:4",
    }

    public = adapter_server.public_module_with_qualified_name(
        module,
        owner={"kind": "CommonModule", "name": "ОбщегоНазначенияБЭДПовтИсп"},
        include_storage=False,
        ordinal=1,
    )

    assert public["kind"] == "common_module"
    assert public["name"] == "ОбщегоНазначенияБЭДПовтИсп"
    assert public["qualified_name"] == "ОбщегоНазначенияБЭДПовтИсп"
    assert public["display_name"] == "ОбщегоНазначенияБЭДПовтИсп"
    assert "storage_name" not in public
    assert "module_id" not in public


def test_public_common_module_without_owner_name_does_not_leak_storage_route() -> None:
    public = adapter_server.public_module_row(
        {
            "module_id": "Config:0422fa60-6cd4-410c-b2af-8792a6a9b199.0#stream:4",
            "name": "0422fa60-6cd4-410c-b2af-8792a6a9b199.0#stream:4",
        },
        include_storage=False,
        ordinal=1,
        owner_kind="CommonModule",
    )

    assert public["name"] == "Общий модуль"
    assert "0422fa60" not in str(public)


def test_public_code_qualified_name_joins_owner_form_and_module() -> None:
    assert adapter_server.public_code_qualified_name(
        owner={"name": "test2"},
        form={"name": "t_Форма"},
        module={"name": "Модуль формы"},
    ) == "test2.t_Форма.Модуль формы"
    assert adapter_server.public_code_qualified_name(
        owner={"name": "tt_Справочник1"},
        module={"name": "Модуль объекта"},
    ) == "tt_Справочник1.Модуль объекта"


def test_form_embedded_module_public_text_removes_container_tail() -> None:
    text = "&НаКлиенте\nПроцедура Команда1(Команда)\nКонецПроцедуры\n\n///----\",\n{4,0,0,0,{#base64:AAAA}}"

    public = adapter_server.form_embedded_module_public_text(text)

    assert public == "&НаКлиенте\nПроцедура Команда1(Команда)\nКонецПроцедуры"


def test_merge_module_owner_context_marks_direct_form_module_and_qualified_name() -> None:
    result = {"module": {"kind": "bsl_container_module", "name": "BSL module"}}
    owner_context = {
        "owner": {"name": "test2", "kind": "CommonForm"},
        "form": {"name": "t_Форма"},
        "module": {"module_name": "Модуль формы", "form": "t_Форма"},
    }

    merged = adapter_server.merge_module_owner_context(result, owner_context)

    assert merged["module"]["kind"] == "form_module"
    assert merged["module"]["name"] == "Модуль формы"
    assert merged["qualified_name"] == "test2.t_Форма.Модуль формы"


def test_merge_module_owner_context_replaces_partial_owner_with_saved_form_context() -> None:
    result = {"module": {"kind": "bsl_container_module", "name": "BSL module"}}
    owner_context = {
        "owner": {"status": "resolved", "kind": "CommonForm", "name": "test2"},
        "form": {"name": "t_Форма"},
        "module": {"kind": "form_module", "name": "Модуль формы"},
        "qualified_name": "test2.t_Форма.Модуль формы",
    }

    merged = adapter_server.merge_module_owner_context(result, owner_context)

    assert merged["owner"]["name"] == "test2"
    assert merged["form"]["name"] == "t_Форма"
    assert merged["qualified_name"] == "test2.t_Форма.Модуль формы"


def test_saved_state_bsl_module_role_uses_owner_kind_for_register() -> None:
    role = adapter_server.saved_state_bsl_module_role("extension-guid__register-guid.0", owner_kind="InformationRegister")

    assert role == {"kind": "record_set_module", "name": "Модуль набора записей", "suffix": "0"}


def test_saved_state_module_search_uses_cached_owner_kind_for_register(monkeypatch: pytest.MonkeyPatch) -> None:
    module_text = "Процедура ПередЗаписью(Отказ)\r\nКонецПроцедуры\r\n".encode("utf-8-sig")
    header = f"\r\n{len(module_text):08x} {len(module_text):08x} 7fffffff \r\n".encode("ascii")
    stored_module = compress_payload(b"prefix" + header + module_text, "raw_deflate")
    stored_descriptor = compress_payload('{"РегистрСведений1"}'.encode("utf-8-sig"), "raw_deflate")
    module_file_name = "extension-guid__register-guid.0"
    descriptor_file_name = "extension-guid__register-guid"

    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {"status": "ok", "files": [{"FileName": module_file_name, "PartCount": 1, "Bytes": len(stored_module)}]},
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_cache_lookup_guid",
        lambda base_id, guid: {"kind": "InformationRegister", "name": "РегистрСведений1", "guid": guid} if guid == "register-guid" else None,
    )

    def fake_read_storage_file_bytes(base_id: str, table: str, requested_file_name: str, *, timeout_seconds: int = 30):
        if requested_file_name == descriptor_file_name:
            return stored_descriptor, {"database": "upo_test"}, None
        return stored_module, {"database": "upo_test"}, None

    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)

    result = adapter_server.metadata_saved_state_modules_search(
        {
            "base_id": "upo_test",
            "tables": ["ConfigCASSave"],
            "query": "ПередЗаписью",
            "limit": 10,
            "include_storage": True,
        }
    )

    module = result["modules"][0]
    assert module["owner"]["kind"] == "InformationRegister"
    assert module["module"] == {"kind": "record_set_module", "name": "Модуль набора записей", "suffix": "0"}
    assert module["qualified_name"] == "РегистрСведений1.Модуль набора записей"
    assert module["streams"][0]["module"] == {"kind": "record_set_module", "name": "Модуль набора записей", "suffix": "0"}
    assert module["streams"][0]["qualified_name"] == "РегистрСведений1.Модуль набора записей"


def test_saved_state_modules_search_treats_empty_query_as_no_filter(monkeypatch: pytest.MonkeyPatch) -> None:
    seen_payloads = []

    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {
            "status": "ok",
            "files": [{"FileName": "extension-guid__object-guid.0", "PartCount": 1, "Bytes": 1}],
        },
    )

    def fake_search_row(**kwargs):
        seen_payloads.append(kwargs["payload"])
        return {"module": {"kind": "object_module", "name": "Модуль объекта"}, "streams": []}

    monkeypatch.setattr(adapter_server, "saved_state_module_search_row", fake_search_row)

    result = adapter_server.metadata_saved_state_modules_search(
        {"base_id": "upo_test", "tables": ["ConfigCASSave"], "query": "", "limit": 5, "include_storage": True}
    )

    assert result["status"] == "ok"
    assert result["counts"]["modules"] == 1
    assert result["query"]["query"] is None
    assert seen_payloads[0]["query"] is None


def test_saved_state_modules_search_pushes_exact_file_filter_into_sql_equality(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    file_name = "fb26cf42-7609-11f1-828f-005056b0d483__77494708-43ea-4956-ac3c-199cfb035ad2.0"
    seen: list[dict[str, Any]] = []

    def fake_storage_files_list(payload: dict[str, Any]) -> dict[str, Any]:
        seen.append(dict(payload))
        return {"status": "ok", "files": [{"FileName": file_name, "PartCount": 1, "Bytes": 1}]}

    monkeypatch.setattr(adapter_server, "storage_files_list", fake_storage_files_list)
    monkeypatch.setattr(
        adapter_server,
        "saved_state_module_search_row",
        lambda **_kwargs: {"source": {"table": "ConfigCASSave", "file_name": file_name}, "streams": []},
    )

    result = adapter_server.metadata_saved_state_modules_search(
        {
            "base_id": "upo_test",
            "tables": ["ConfigCASSave"],
            "file_name": file_name,
            "limit": 1,
            "scan_limit": 10,
            "include_storage": True,
        }
    )

    assert result["counts"]["modules"] == 1
    assert seen[0]["exact_file_name"] == file_name
    assert "prefix" not in seen[0]


def test_saved_state_modules_search_accepts_semantic_layer(monkeypatch: pytest.MonkeyPatch) -> None:
    seen_tables: list[str] = []

    def fake_storage_files_list(payload: dict[str, Any]) -> dict[str, Any]:
        seen_tables.append(payload["table"])
        return {"status": "ok", "files": []}

    monkeypatch.setattr(adapter_server, "storage_files_list", fake_storage_files_list)
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_modules_owner_guid_from_selector",
        lambda *_args, **_kwargs: ("owner-guid", {"status": "resolved"}),
    )

    result = adapter_server.metadata_saved_state_modules_search(
        {
            "base_id": "upo_test",
            "layer": "base_saved_state",
            "ref": "CommonModule.Интеграция",
            "query": "Отправить",
        }
    )

    assert result["status"] == "ok"
    assert result["source"] == {"kind": "saved_state"}
    assert result["query"]["layer"] == "base_saved_state"
    assert seen_tables == ["ConfigSave"]


def test_saved_state_modules_search_rejects_layer_and_tables_together() -> None:
    result = adapter_server.metadata_saved_state_modules_search(
        {
            "base_id": "upo_test",
            "layer": "extension_saved_state",
            "tables": ["ConfigCASSave"],
        }
    )

    assert result["status"] == "invalid_argument"
    assert result["argument"] == "tables"


def test_saved_state_modules_search_scopes_extension_to_cassave_prefix(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    seen_file_list_payloads = []
    seen_rows = []

    monkeypatch.setattr(
        adapter_server,
        "extension_map_by_guid",
        lambda base_id: {extension_guid: {"guid": extension_guid, "name": "test2"}},
    )

    def fake_storage_files_list(payload):
        seen_file_list_payloads.append(payload)
        return {
            "status": "ok",
            "files": [
                {"FileName": f"{extension_guid}__object-guid.0", "PartCount": 1, "Bytes": 1},
                {"FileName": "base-object-guid.0", "PartCount": 1, "Bytes": 1},
            ],
        }

    monkeypatch.setattr(adapter_server, "storage_files_list", fake_storage_files_list)

    def fake_search_row(**kwargs):
        seen_rows.append(kwargs["file_name"])
        return {"module": {"kind": "object_module", "name": "Модуль объекта"}, "streams": []}

    monkeypatch.setattr(adapter_server, "saved_state_module_search_row", fake_search_row)

    result = adapter_server.metadata_saved_state_modules_search(
        {
            "base_id": "upo_test",
            "tables": ["ConfigCASSave", "ConfigSave"],
            "extension": "test2",
            "query": "",
            "limit": 5,
            "include_storage": True,
        }
    )

    assert result["status"] == "ok"
    assert result["source"]["tables"] == ["ConfigCASSave"]
    assert result["query"]["extension"] == "test2"
    assert seen_file_list_payloads == [
        {"base_id": "upo_test", "table": "ConfigCASSave", "limit": 1000, "diagnostic": True, "timeout_seconds": 60, "prefix": f"{extension_guid}__"}
    ]
    assert seen_rows == [f"{extension_guid}__object-guid.0"]


@pytest.mark.parametrize("requested_extension", [None, "test2"])
def test_saved_state_modules_search_preserves_public_extension_name(
    monkeypatch: pytest.MonkeyPatch,
    requested_extension: str | None,
) -> None:
    extension_guid = "FB26CF42-7609-11F1-828F-005056B0D483"
    file_name = f"{extension_guid.lower()}__form-guid.0"

    monkeypatch.setattr(
        adapter_server,
        "extension_map_by_guid",
        lambda base_id: {extension_guid: {"guid": extension_guid, "name": "test2"}},
    )
    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {
            "status": "ok",
            "files": [{"FileName": file_name, "PartCount": 1, "Bytes": 1}],
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "saved_state_module_search_row",
        lambda **kwargs: {
            "table": "ConfigCASSave",
            "file_name": file_name,
            "owner": {"status": "resolved", "kind": "Catalog", "name": "test2"},
            "form": {"name": "t_Форма"},
            "module": {"kind": "form_module", "name": "Модуль формы"},
            "qualified_name": "test2.t_Форма.Модуль формы",
            "streams": [
                {
                    "preview": "Процедура Команда()\nКонецПроцедуры",
                    "match": {"query": None, "in_text": False},
                }
            ],
        },
    )

    payload = {
        "base_id": "upo_test",
        "tables": ["ConfigCASSave"],
        "limit": 5,
    }
    if requested_extension:
        payload["extension"] = requested_extension
    result = adapter_server.metadata_saved_state_modules_search(payload)

    assert result["status"] == "ok"
    assert result["counts"]["modules"] == 1
    module = result["modules"][0]
    assert module["selector"]["extension"] == "test2"
    assert module["streams"][0]["write_plan_target"]["extension"] == "test2"
    assert "extension_guid" not in str(module)
    assert extension_guid.lower() not in str(module).lower()


def test_saved_state_forms_search_scopes_extension_to_cassave_prefix(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    seen_file_list_payloads = []
    seen_rows = []

    monkeypatch.setattr(
        adapter_server,
        "extension_map_by_guid",
        lambda base_id: {extension_guid: {"guid": extension_guid, "name": "test2"}},
    )

    def fake_storage_files_list(payload):
        seen_file_list_payloads.append(payload)
        return {
            "status": "ok",
            "files": [
                {"FileName": f"{extension_guid}__form-guid.0", "PartCount": 1, "Bytes": 1},
                {"FileName": "base-form-guid.0", "PartCount": 1, "Bytes": 1},
            ],
        }

    monkeypatch.setattr(adapter_server, "storage_files_list", fake_storage_files_list)

    def fake_form_row(**kwargs):
        seen_rows.append(kwargs["file_name"])
        return {"name": "t_Форма", "form": {"name": "t_Форма"}}

    monkeypatch.setattr(adapter_server, "saved_state_form_search_row", fake_form_row)

    result = adapter_server.metadata_saved_state_forms_search(
        {
            "base_id": "upo_test",
            "tables": ["ConfigCASSave", "ConfigSave"],
            "extension": "test2",
            "limit": 5,
            "include_storage": True,
        }
    )

    assert result["status"] == "ok"
    assert result["source"]["tables"] == ["ConfigCASSave"]
    assert result["query"]["extension"] == "test2"
    assert seen_file_list_payloads == [
        {"base_id": "upo_test", "table": "ConfigCASSave", "limit": 1000, "diagnostic": True, "timeout_seconds": 60, "prefix": f"{extension_guid}__"}
    ]
    assert seen_rows == [f"{extension_guid}__form-guid.0"]


def test_extension_saved_state_common_form_has_public_name_and_state(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"

    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_forms_search",
        lambda payload: {
            "status": "ok",
            "forms": [
                {
                    "table": "ConfigCASSave",
                    "file_name": f"{extension_guid}__form-guid.0",
                    "form": {
                        "name": "t_Форма",
                        "identity": {"name": "t_Форма", "guid": "form-guid", "source": "saved_state_descriptor"},
                    },
                }
            ],
            "counts": {"scanned": 1},
        },
    )

    matches, errors, stats = adapter_server.extension_saved_state_object_matches(
        base_id="upo_test",
        payload={"extension": "test2"},
        query="",
        kind_filter="CommonForm",
        guid_filter="",
        extension_guid=extension_guid,
        active_guid_keys=set(),
        limit=10,
        scan_limit=100,
        timeout_seconds=60,
        include_storage=False,
    )

    assert errors == []
    assert stats["saved_state_rows"] == 1
    assert matches[0]["qualified_name"] == "test2.t_Форма"
    assert matches[0]["current_state"] == {"source": "saved_state", "activation_state": "not_activated"}


def test_modules_search_marks_extension_saved_only_module(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    saved_file = f"{extension_guid}__caee4437-14b6-436f-98e3-360c4577f20d.0"

    class FakeStreamClassifier:
        pass

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
    monkeypatch.setattr(adapter_server, "extension_map_by_guid", lambda base_id: {extension_guid: {"name": "test2", "guid": extension_guid}})
    monkeypatch.setattr(
        adapter_server,
        "extension_objects_find",
        lambda payload: {"status": "ok", "objects": [{"guid": "77494708-43ea-4956-ac3c-199cfb035ad2"}]},
    )
    monkeypatch.setattr(
        adapter_server,
        "extension_definition_guid_sources",
        lambda *args, **kwargs: (
            {
                "77494708-43ea-4956-ac3c-199cfb035ad2": [
                    {
                        "extension": {"name": "test2", "guid": extension_guid},
                        "storage_role": "DataProcessor",
                    }
                ]
            },
            None,
        ),
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_modules",
        lambda *args, **kwargs: {"status": "ok", "object": {}, "modules": []},
    )
    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {"status": "ok", "files": [{"FileName": saved_file}], "counts": {"files": 1}},
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_files_bytes",
        lambda *args, **kwargs: ({saved_file: b"payload"}, {"database": "upo_test"}, None),
    )
    monkeypatch.setattr(adapter_server, "payload_text_from_bytes", lambda data: {"status": "ok", "text": ""})
    monkeypatch.setattr(adapter_server, "extract_bsl_text_from_container", lambda text, **kwargs: ("", {"status": "not_found"}))
    monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
    FakeStreamClassifier.classify_payload = staticmethod(
        lambda data, include_text=True: {
            "role": "bsl_module_payload",
            "stream_blocks": [{"text": "Процедура SavedOnlyProbe()\nКонецПроцедуры\n"}],
        }
    )

    result = adapter_server.search_modules({"base_id": "upo_test", "extension": "test2", "query": "SavedOnlyProbe", "scan_limit": 10})

    assert result["status"] == "ok"
    assert result["matches"][0]["read_selector"]["module_ref"] == f"ConfigCASSave:{saved_file}#stream:0"
    assert result["matches"][0]["activation_state"] == "saved_only"


def test_modules_search_marks_extension_saved_override_module(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    saved_file = f"{extension_guid}__77494708-43ea-4956-ac3c-199cfb035ad2.0"

    class FakeStreamClassifier:
        pass

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
    monkeypatch.setattr(adapter_server, "extension_map_by_guid", lambda base_id: {extension_guid: {"name": "test2", "guid": extension_guid}})
    monkeypatch.setattr(
        adapter_server,
        "extension_objects_find",
        lambda payload: {"status": "ok", "objects": [{"guid": "77494708-43ea-4956-ac3c-199cfb035ad2"}]},
    )
    monkeypatch.setattr(adapter_server, "extension_definition_guid_sources", lambda *args, **kwargs: ({}, None))
    monkeypatch.setattr(adapter_server, "metadata_object_modules", lambda *args, **kwargs: {"status": "ok", "object": {}, "modules": []})
    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {"status": "ok", "files": [{"FileName": saved_file}], "counts": {"files": 1}},
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_files_bytes",
        lambda *args, **kwargs: ({saved_file: b"payload"}, {"database": "upo_test"}, None),
    )
    monkeypatch.setattr(adapter_server, "payload_text_from_bytes", lambda data: {"status": "ok", "text": ""})
    monkeypatch.setattr(adapter_server, "extract_bsl_text_from_container", lambda text, **kwargs: ("", {"status": "not_found"}))
    monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
    FakeStreamClassifier.classify_payload = staticmethod(
        lambda data, include_text=True: {
            "role": "bsl_module_payload",
            "stream_blocks": [{"text": "Процедура SavedOverrideProbe()\nКонецПроцедуры\n"}],
        }
    )

    result = adapter_server.search_modules({"base_id": "upo_test", "extension": "test2", "query": "SavedOverrideProbe", "scan_limit": 10})

    assert result["status"] == "ok"
    assert result["matches"][0]["activation_state"] == "saved_override"


def test_modules_search_accepts_object_type_name_aliases(monkeypatch: pytest.MonkeyPatch) -> None:
    seen_payloads: list[dict[str, Any]] = []

    def fake_metadata_object_modules(payload: dict[str, Any]) -> dict[str, Any]:
        seen_payloads.append(payload)
        return {
            "status": "ok",
            "object": {
                "kind": "DataProcessor",
                "name": "ТестоваяОбработка",
                "synonym": "Тестовая обработка",
                "guid": "22222222-2222-2222-2222-222222222222",
            },
            "modules": [{"name": "Модуль объекта", "module_id": "ConfigCAS:module-guid#stream:0"}],
        }

    class FakeStreamClassifier:
        pass

    monkeypatch.setattr(adapter_server, "metadata_object_modules", fake_metadata_object_modules)
    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda *args, **kwargs: (b"payload", {"database": "upo_test"}, None),
    )
    monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
    FakeStreamClassifier.classify_payload = staticmethod(
        lambda data, include_text=True: {
            "role": "bsl_module_payload",
            "stream_blocks": [{"text": "Процедура ЦелеваяПроцедура()\nКонецПроцедуры\n"}],
        }
    )

    result = adapter_server.call_method(
        "modules.search",
        {
            "base_id": "upo_test",
            "object_type": "Обработка",
            "object_name": "ТестоваяОбработка",
            "query": "ЦелеваяПроцедура",
        },
    )

    assert result["status"] == "ok"
    assert seen_payloads[0]["kind"] == "DataProcessor"
    assert seen_payloads[0]["name"] == "ТестоваяОбработка"
    assert result["query"]["scope"] == "object_modules"
    assert result["matches"][0]["owner"]["kind"] == "DataProcessor"


def test_modules_search_includes_saved_object_command_modules(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
    command_guid = "a9b2fc38-86b6-4586-8aca-29b92bdd86de"
    module_ref = f"ConfigCASSave:{extension_guid}__{command_guid}.2"

    monkeypatch.setattr(
        adapter_server,
        "metadata_object_modules",
        lambda payload: {
            "status": "ok",
            "object": {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid},
            "modules": [],
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_commands",
        lambda payload: {
            "status": "ok",
            "object_commands": [
                {
                    "guid": command_guid,
                    "name": "Команда1",
                    "status": "ok",
                    "read_selector": {
                        "method": "modules.read",
                        "base_id": "upo_test",
                        "kind": "CalculationRegister",
                        "guid": object_guid,
                        "module_ref": module_ref,
                    },
                }
            ],
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "read_module",
        lambda payload: {
            "status": "ok",
            "text": "&НаКлиенте\nПроцедура ОбработкаКоманды()\nКонецПроцедуры\n",
            "module": {"kind": "command_module", "name": "Модуль команды"},
        },
    )

    result = adapter_server.search_modules(
        {
            "base_id": "upo_test",
            "kind": "CalculationRegister",
            "guid": object_guid,
            "table": "ConfigCASSave",
            "extension_guid": extension_guid,
            "query": "ОбработкаКоманды",
            "state": "save",
        }
    )

    assert result["status"] == "ok"
    assert result["counts"]["command_modules"] == 1
    assert result["counts"]["owner_resolved"] == 1
    assert result["counts"]["complete"] is True
    assert result["matches"][0]["module"]["kind"] == "command_module"
    assert result["matches"][0]["module"]["command"] == "Команда1"
    assert result["matches"][0]["read_selector"]["module_ref"] == module_ref
    assert result["matches"][0]["activation_state"] == "saved_state"


def test_modules_search_direct_call_accepts_public_ref(monkeypatch: pytest.MonkeyPatch) -> None:
    seen_payloads: list[dict[str, Any]] = []

    def fake_metadata_object_modules(payload: dict[str, Any]) -> dict[str, Any]:
        seen_payloads.append(payload)
        return {
            "status": "ok",
            "object": {
                "kind": "DataProcessor",
                "name": "ТестоваяОбработка",
                "guid": "22222222-2222-2222-2222-222222222222",
            },
            "modules": [],
        }

    monkeypatch.setattr(adapter_server, "metadata_object_modules", fake_metadata_object_modules)

    result = adapter_server.search_modules(
        {
            "base_id": "upo_test",
            "ref": "Обработка.ТестоваяОбработка",
            "query": "ЦелеваяПроцедура",
        }
    )

    assert result["status"] == "ok"
    assert seen_payloads[0]["kind"] == "DataProcessor"
    assert seen_payloads[0]["name"] == "ТестоваяОбработка"
    assert result["query"]["scope"] == "object_modules"


def test_modules_search_owner_resolution_scans_all_module_capable_kinds(monkeypatch: pytest.MonkeyPatch) -> None:
    module_ref = "ConfigCAS:module-guid#stream:0"

    class FakeStreamClassifier:
        pass

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {"status": "ok", "files": [{"FileName": "module-guid"}], "counts": {"files": 1}},
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_files_bytes",
        lambda *args, **kwargs: ({"module-guid": b"payload"}, {"database": "upo_test"}, None),
    )
    monkeypatch.setattr(adapter_server, "payload_text_from_bytes", lambda data: {"status": "ok", "text": ""})
    monkeypatch.setattr(adapter_server, "extract_bsl_text_from_container", lambda text, **kwargs: ("", {"status": "not_found"}))
    monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
    FakeStreamClassifier.classify_payload = staticmethod(
        lambda data, include_text=True: {
            "role": "bsl_module_payload",
            "stream_blocks": [{"text": "Функция НайтиСтроку()\nВозврат 1;\nКонецФункции\n"}],
        }
    )

    def fake_list_objects(kind: str, **kwargs: Any) -> dict[str, Any]:
        if kind != "InformationRegister":
            return {"status": "ok", "objects": []}
        return {
            "status": "ok",
            "objects": [
                {
                    "kind": "InformationRegister",
                    "name": "ТестовыйРегистр",
                    "synonym": "Тестовый регистр",
                    "guid": "33333333-3333-3333-3333-333333333333",
                }
            ],
        }

    def fake_metadata_object_modules(payload: dict[str, Any]) -> dict[str, Any]:
        if payload.get("kind") != "InformationRegister":
            return {"status": "ok", "object": {}, "modules": []}
        return {
            "status": "ok",
            "object": {
                "kind": "InformationRegister",
                "name": "ТестовыйРегистр",
                "synonym": "Тестовый регистр",
                "guid": "33333333-3333-3333-3333-333333333333",
            },
            "modules": [{"name": "Модуль менеджера", "module_id": module_ref}],
        }

    monkeypatch.setattr(adapter_server, "list_objects", fake_list_objects)
    monkeypatch.setattr(adapter_server, "metadata_object_modules", fake_metadata_object_modules)

    result = adapter_server.search_modules(
        {
            "base_id": "upo_test",
            "query": "НайтиСтроку",
            "scan_limit": 10,
            "resolve_owners": True,
            "owner_scan_limit": 40,
            "state": "active",
        }
    )

    assert result["status"] == "ok"
    assert result["counts"]["owner_resolved"] == 1
    assert result["counts"]["owner_indexed_module_refs"] == 1
    assert result["diagnostics"]["owner_resolution"]["strategy"] == "live_metadata_scan"
    assert result["diagnostics"]["owner_resolution"]["indexed_module_refs"] == 1
    match = result["matches"][0]
    assert match["owner"]["kind"] == "InformationRegister"
    assert match["owner"]["name"] == "ТестовыйРегистр"
    assert match["read_selector"]["kind"] == "InformationRegister"
    assert match["read_selector"]["name"] == "ТестовыйРегистр"
    assert match["read_selector"]["ref"] == "InformationRegister.ТестовыйРегистр"
    assert match["read_selector"]["method"] == "modules.read"
    assert match["read_selector"]["module_ordinal"] == 1


def test_modules_search_reports_owner_scan_limit_hit(monkeypatch: pytest.MonkeyPatch) -> None:
    class FakeStreamClassifier:
        pass

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {"status": "ok", "files": [{"FileName": "module-guid"}], "counts": {"files": 1}},
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_files_bytes",
        lambda *args, **kwargs: ({"module-guid": b"payload"}, {"database": "upo_test"}, None),
    )
    monkeypatch.setattr(adapter_server, "payload_text_from_bytes", lambda data: {"status": "ok", "text": ""})
    monkeypatch.setattr(adapter_server, "extract_bsl_text_from_container", lambda text, **kwargs: ("", {"status": "not_found"}))
    monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
    FakeStreamClassifier.classify_payload = staticmethod(
        lambda data, include_text=True: {
            "role": "bsl_module_payload",
            "stream_blocks": [{"text": "Процедура НеНайденныйВладелец()\nКонецПроцедуры\n"}],
        }
    )
    monkeypatch.setattr(
        adapter_server,
        "list_objects",
        lambda kind, **kwargs: {
            "status": "ok",
            "objects": [
                {
                    "kind": kind,
                    "name": "ПервыйОбъект",
                    "guid": "44444444-4444-4444-4444-444444444444",
                }
            ],
        },
    )
    monkeypatch.setattr(adapter_server, "metadata_object_modules", lambda payload: {"status": "ok", "object": {}, "modules": []})

    result = adapter_server.search_modules(
        {
            "base_id": "upo_test",
            "query": "НеНайденныйВладелец",
            "resolve_owners": True,
            "owner_scan_limit": 1,
            "scan_limit": 10,
        }
    )

    assert result["status"] == "ok"
    assert result["counts"]["owner_unresolved"] == 1
    assert result["counts"]["owner_scan_limit_hit"] is True
    assert result["diagnostics"]["owner_resolution"]["owner_scan_limit_hit"] is True
    assert result["diagnostics"]["owner_resolution"]["owner_objects_scanned"] == 1


def test_modules_search_public_call_preserves_read_selector_module_ref(monkeypatch: pytest.MonkeyPatch) -> None:
    class FakeStreamClassifier:
        pass

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {"status": "ok", "files": [{"FileName": "module-guid"}], "counts": {"files": 1}},
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_files_bytes",
        lambda *args, **kwargs: ({"module-guid": b"payload"}, {"database": "upo_test"}, None),
    )
    monkeypatch.setattr(adapter_server, "payload_text_from_bytes", lambda data: {"status": "ok", "text": ""})
    monkeypatch.setattr(adapter_server, "extract_bsl_text_from_container", lambda text, **kwargs: ("", {"status": "not_found"}))
    monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
    FakeStreamClassifier.classify_payload = staticmethod(
        lambda data, include_text=True: {
            "role": "bsl_module_payload",
            "stream_blocks": [{"text": "Процедура ПубличныйСелектор()\nКонецПроцедуры\n"}],
        }
    )

    result = adapter_server.call_method("modules.search", {"base_id": "upo_test", "query": "ПубличныйСелектор", "state": "active"})

    assert result["status"] == "ok"
    selector = result["matches"][0]["read_selector"]
    assert selector["method"] == "modules.read"
    assert selector["module_ref"] == "ConfigCAS:module-guid#stream:0"
    assert "module_id" not in result["matches"][0]
    assert "file_name" not in result["matches"][0]


def test_code_search_exposes_scan_and_owner_counts(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "search_modules",
        lambda payload: {
            "status": "partial",
            "source": {"kind": "live_metadata"},
            "matches": [
                {
                    "snippet": {"text": "Запрос = Новый Запрос;", "offset": 0},
                    "owner": {"status": "unresolved", "kind": None, "name": None},
                    "origin": {"source": "cas_reference", "status": "owner_unresolved", "write_surface": "requires_owner_resolution"},
                    "module": {"name": "Модуль БСЛ"},
                    "read_selector": {"base_id": "upo_test", "module_ref": "ConfigCAS:module-guid#stream:0"},
                }
            ],
            "counts": {
                "truncated": True,
                "complete": False,
                "scan_limit_hit": True,
                "owner_resolved": 0,
                "owner_unresolved": 1,
                "owner_scan_limit_hit": True,
                "owner_indexed_module_refs": 0,
            },
            "diagnostics": {"message": "limited"},
        },
    )

    result = adapter_server.code_search({"base_id": "upo_test", "query": "Новый Запрос", "limit": 10})

    assert result["status"] == "partial"
    assert result["counts"]["truncated"] is True
    assert result["counts"]["complete"] is False
    assert result["counts"]["scan_limit_hit"] is True
    assert result["counts"]["owner_resolved"] == 0
    assert result["counts"]["owner_unresolved"] == 1
    assert result["counts"]["owner_scan_limit_hit"] is True
    assert result["counts"]["owner_indexed_module_refs"] == 0
    assert result["items"][0]["read_selector"]["method"] == "code.read"
    assert result["items"][0]["read_selector"]["module_ref"] == "ConfigCAS:module-guid#stream:0"
    assert result["items"][0]["origin"]["source"] == "cas_reference"
    assert result["items"][0]["origin"]["write_surface"] == "requires_owner_resolution"


def test_code_search_uses_effective_working_view_by_default(monkeypatch: pytest.MonkeyPatch) -> None:
    seen_payload: dict[str, Any] = {}

    def fake_search_modules(payload: dict[str, Any]) -> dict[str, Any]:
        seen_payload.update(payload)
        return {"status": "not_found", "source": {"kind": "logical"}, "matches": [], "counts": {}, "diagnostics": {}}

    monkeypatch.setattr(adapter_server, "search_modules", fake_search_modules)

    result = adapter_server.code_search({"base_id": "upo_test", "query": "Тест"})

    assert seen_payload["state"] == "working"
    assert result["configuration_view"]["name"] == "effective_working"
    assert "not claimed to be the currently applied runtime" in result["configuration_view"]["meaning"]


def test_code_search_accepts_runtime_applied_configuration_view(monkeypatch: pytest.MonkeyPatch) -> None:
    seen_payload: dict[str, Any] = {}

    def fake_search_modules(payload: dict[str, Any]) -> dict[str, Any]:
        seen_payload.update(payload)
        return {"status": "not_found", "source": {"kind": "logical"}, "matches": [], "counts": {}, "diagnostics": {}}

    monkeypatch.setattr(adapter_server, "search_modules", fake_search_modules)

    result = adapter_server.code_search(
        {"base_id": "upo_test", "query": "Тест", "configuration_view": "runtime_applied"}
    )

    assert seen_payload["state"] == "active"
    assert result["configuration_view"]["name"] == "runtime_applied"


def test_code_read_rejects_conflicting_configuration_view(monkeypatch: pytest.MonkeyPatch) -> None:
    result = adapter_server.code_read(
        {
            "base_id": "upo_test",
            "configuration_view": "runtime_applied",
            "state": "working",
            "module_id": "Config:test.0",
        }
    )

    assert result["status"] == "invalid_argument"
    assert result["argument"] == "configuration_view"


def test_code_search_accepts_module_ordinal_without_tuple_response(monkeypatch: pytest.MonkeyPatch) -> None:
    seen_payload: dict[str, Any] = {}

    def fake_search_modules(payload: dict[str, Any]) -> dict[str, Any]:
        seen_payload.update(payload)
        return {
            "status": "ok",
            "source": {"kind": "live_metadata"},
            "matches": [
                {
                    "snippet": {"text": "Процедура Проверка() Экспорт", "offset": 0},
                    "owner": {"status": "resolved", "kind": "Document", "name": "ТестовыйДокумент"},
                    "module": {"name": "Модуль объекта", "module_ordinal": 1},
                    "read_selector": {
                        "base_id": "upo_test",
                        "method": "modules.read",
                        "kind": "Document",
                        "name": "ТестовыйДокумент",
                        "module_ordinal": 1,
                    },
                }
            ],
            "counts": {"matches": 1, "complete": True, "scan_limit_hit": False},
            "diagnostics": {},
        }

    monkeypatch.setattr(adapter_server, "search_modules", fake_search_modules)

    result = adapter_server.code_search(
        {
            "base_id": "upo_test",
            "kind": "Document",
            "name": "ТестовыйДокумент",
            "query": "Процедура",
            "module_ordinal": 1,
        }
    )

    assert isinstance(result, dict)
    assert result["schema"] == "onec_code_search.v1"
    assert result["status"] == "ok"
    assert seen_payload["module_ordinal"] == 1
    assert result["items"][0]["read_selector"]["method"] == "code.read"


def test_code_search_reads_saved_state_common_form_without_storage(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_modules_search",
        lambda payload: {
            "schema": "onec_saved_state_module_search.v1",
            "status": "ok",
            "source": {"kind": "live_sql", "tables": ["ConfigCASSave"]},
            "modules": [
                {
                    "table": "ConfigCASSave",
                    "file_name": "form.0",
                    "streams": [
                        {
                            "module_ref": "ConfigCASSave:form.0",
                            "module_path": "2",
                            "preview": "\r\n&НаКлиенте\r\nПроцедура ЗаменаДомена(Команда)\r\n\t// Отредактировано адаптером\r\nКонецПроцедуры\r\n",
                        }
                    ],
                }
            ],
            "counts": {"modules": 1},
        },
    )

    result = adapter_server.code_search(
        {
            "base_id": "upo_test",
            "object_type": "CommonForm",
            "object_name": "t_Форма",
            "query": "Отредактировано адаптером",
        }
    )

    assert result["status"] == "ok"
    assert result["source"]["kind"] == "saved_state"
    assert result["current_state"] == {"source": "saved_state", "activation_state": "not_activated"}
    assert result["items"][0]["current_state"] == {"source": "saved_state", "activation_state": "not_activated"}
    assert result["items"][0]["read_selector"] == {
        "method": "code.read",
        "base_id": "upo_test",
        "object_type": "CommonForm",
        "object_name": "t_Форма",
        "include_text": True,
        "max_chars": 100000,
    }
    assert "storage" not in result["items"][0]


def test_code_search_hides_form_embedded_module_trailing_marker(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_modules_search",
        lambda payload: {
            "schema": "onec_saved_state_module_search.v1",
            "status": "ok",
            "source": {"kind": "live_sql", "tables": ["ConfigCASSave"]},
            "modules": [
                {
                    "table": "ConfigCASSave",
                    "file_name": "form.0",
                    "streams": [
                        {
                            "module_ref": "ConfigCASSave:form.0",
                            "module_path": "2",
                            "preview": "Процедура Проверка()\nКонецПроцедуры\n\n///----",
                        }
                    ],
                }
            ],
            "counts": {"modules": 1},
        },
    )

    result = adapter_server.code_search(
        {
            "base_id": "upo_test",
            "object_type": "CommonForm",
            "object_name": "t_Форма",
            "query": "///----",
        }
    )

    assert result["status"] == "not_found"
    assert result["items"] == []


def test_code_read_hides_form_embedded_module_trailing_marker(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_modules_search",
        lambda payload: {
            "schema": "onec_saved_state_module_search.v1",
            "status": "ok",
            "modules": [{"streams": [{"preview": "&НаКлиенте\nПроцедура Проверка()\nКонецПроцедуры\n\n///----"}]}],
        },
    )

    result = adapter_server.code_read(
        {
            "base_id": "upo_test",
            "object_type": "CommonForm",
            "object_name": "t_Форма",
            "state": "working",
        }
    )

    assert result["status"] == "ok"
    assert "///----" not in result["text"]
    assert result["text"].endswith("КонецПроцедуры")


def test_code_search_state_both_merges_saved_common_form_and_active(monkeypatch: pytest.MonkeyPatch) -> None:
    seen_active_payload: dict[str, Any] = {}
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_modules_search",
        lambda payload: {
            "schema": "onec_saved_state_module_search.v1",
            "status": "ok",
            "source": {"kind": "live_sql", "tables": ["ConfigCASSave"]},
            "modules": [
                {
                    "table": "ConfigCASSave",
                    "file_name": "form.0",
                    "streams": [
                        {
                            "module_ref": "ConfigCASSave:form.0",
                            "module_path": "2",
                            "preview": "Процедура ЗаменаДомена(Команда)\n\t// saved\nКонецПроцедуры\n",
                        }
                    ],
                }
            ],
            "counts": {"modules": 1},
        },
    )

    def fake_search_modules(payload: dict[str, Any]) -> dict[str, Any]:
        seen_active_payload.update(payload)
        return {
            "status": "ok",
            "source": {"kind": "live_metadata"},
            "matches": [
                {
                    "snippet": {"text": "// active", "offset": 0},
                    "owner": {"status": "resolved", "kind": "CommonForm", "name": "t_Форма"},
                    "origin": {"source": "active"},
                    "module": {"name": "Модуль формы"},
                    "read_selector": {"base_id": "upo_test", "method": "modules.read", "kind": "CommonForm", "name": "t_Форма"},
                }
            ],
            "counts": {"complete": True, "scan_limit_hit": False},
        }

    monkeypatch.setattr(adapter_server, "search_modules", fake_search_modules)

    result = adapter_server.code_search(
        {
            "base_id": "upo_test",
            "object_type": "CommonForm",
            "object_name": "t_Форма",
            "query": "ЗаменаДомена",
            "state": "both",
        }
    )

    assert seen_active_payload["state"] == "active"
    assert result["status"] == "ok"
    assert result["current_state"] == {"source": "both", "activation_state": "mixed"}
    assert result["counts"]["saved_matches"] == 1
    assert result["counts"]["active_matches"] == 1
    assert result["comparison"]["both_present"] is True
    assert result["items"][0]["current_state"] == {"source": "saved_state", "activation_state": "not_activated"}
    assert result["items"][1]["current_state"] == {"source": "active", "activation_state": "active"}


def test_code_read_reads_saved_state_common_form_routine(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_modules_search",
        lambda payload: {
            "schema": "onec_saved_state_module_search.v1",
            "status": "ok",
            "modules": [
                {
                    "table": "ConfigCASSave",
                    "file_name": "form.0",
                    "streams": [
                        {
                            "module_ref": "ConfigCASSave:form.0",
                            "module_path": "2",
                            "preview": "\r\n&НаКлиенте\r\nПроцедура ЗаменаДомена(Команда)\r\n\t// Отредактировано адаптером\r\nКонецПроцедуры\r\n",
                        }
                    ],
                }
            ],
        },
    )

    result = adapter_server.code_read(
        {
            "base_id": "upo_test",
            "object_type": "CommonForm",
            "object_name": "t_Форма",
            "routine_name": "ЗаменаДомена",
            "include_line_numbers": True,
        }
    )

    assert result["schema"] == "onec_code_read.v1"
    assert result["status"] == "ok"
    assert result["source"]["kind"] == "saved_state"
    assert result["current_state"] == {"source": "saved_state", "activation_state": "not_activated"}
    assert "Отредактировано адаптером" in result["text"]
    assert result["line"] == 3


def test_code_read_state_active_skips_saved_state_common_form(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_modules_search",
        lambda payload: (_ for _ in ()).throw(AssertionError("state=active must not read saved-state")),
    )
    monkeypatch.setattr(
        adapter_server,
        "read_module",
        lambda payload: {"status": "ok", "text": "active text"},
    )

    result = adapter_server.code_read(
        {
            "base_id": "upo_test",
            "object_type": "CommonForm",
            "object_name": "t_Форма",
            "state": "active",
        }
    )

    assert result["status"] == "ok"
    assert result["current_state"] == {"source": "active", "activation_state": "active"}
    assert result["text"] == "active text"


def test_code_read_state_active_not_found_keeps_active_state(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_modules_search",
        lambda payload: (_ for _ in ()).throw(AssertionError("state=active must not read saved-state")),
    )
    monkeypatch.setattr(
        adapter_server,
        "read_module",
        lambda payload: {"schema": "onec_metadata_object.v1", "method": "modules.read", "status": "not_found", "error": "not_found"},
    )

    result = adapter_server.code_read(
        {
            "base_id": "upo_test",
            "object_type": "CommonForm",
            "object_name": "t_Форма",
            "state": "active",
        }
    )

    assert result["status"] == "not_found"
    assert result["method"] == "code.read"
    assert result["current_state"] == {"source": "active", "activation_state": "active"}


def test_code_read_state_save_does_not_fallback_to_active(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_modules_search",
        lambda payload: {"schema": "onec_saved_state_module_search.v1", "status": "ok", "modules": []},
    )
    monkeypatch.setattr(
        adapter_server,
        "read_module",
        lambda payload: (_ for _ in ()).throw(AssertionError("state=save must not read active fallback")),
    )

    result = adapter_server.code_read(
        {
            "base_id": "upo_test",
            "object_type": "CommonForm",
            "object_name": "t_Форма",
            "state": "save",
        }
    )

    assert result["status"] == "not_found"
    assert result["error"] == "saved_state_code_not_found"
    assert result["current_state"] == {"source": "saved_state", "activation_state": "not_activated"}


def test_code_read_state_both_returns_saved_and_active_layers(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_modules_search",
        lambda payload: {
            "schema": "onec_saved_state_module_search.v1",
            "status": "ok",
            "modules": [
                {
                    "streams": [
                        {
                            "preview": "\r\nПроцедура ЗаменаДомена(Команда)\r\n\t// saved\r\nКонецПроцедуры\r\n",
                        }
                    ],
                }
            ],
        },
    )
    def fake_read_module(payload: dict[str, object]) -> dict[str, object]:
        assert payload["state"] == "active"
        return {
            "schema": "onec_code_read.v1",
            "method": "code.read",
            "status": "ok",
            "text": "Процедура ЗаменаДомена(Команда)\n\t// active\nКонецПроцедуры\n",
            "current_state": {"source": "active", "activation_state": "active"},
        }

    monkeypatch.setattr(adapter_server, "read_module", fake_read_module)

    result = adapter_server.code_read(
        {
            "base_id": "upo_test",
            "object_type": "CommonForm",
            "object_name": "t_Форма",
            "routine_name": "ЗаменаДомена",
            "state": "both",
        }
    )

    assert result["status"] == "ok"
    assert result["current_state"] == {"source": "both", "activation_state": "mixed"}
    assert result["comparison"]["both_present"] is True
    assert result["comparison"]["differs"] is True
    assert result["layers"][0]["source"] == "saved_state"
    assert result["layers"][0]["current_state"] == {"source": "saved_state", "activation_state": "not_activated"}
    assert result["layers"][1]["source"] == "active"
    assert result["layers"][1]["current_state"] == {"source": "active", "activation_state": "active"}
    assert result["text_source"] == "saved_state"


def test_code_read_state_both_treats_saved_module_ref_as_saved_layer(monkeypatch: pytest.MonkeyPatch) -> None:
    seen_payloads: list[dict[str, Any]] = []

    def fake_read_module(payload: dict[str, Any]) -> dict[str, Any]:
        seen_payloads.append(dict(payload))
        if payload.get("state") == "save":
            assert payload["module_ref"] == "ConfigCASSave:object__module.0#stream:4"
            return {
                "schema": "onec_module_read.v1",
                "status": "ok",
                "text": "Процедура ИзSave()\nКонецПроцедуры\n",
                "origin": {"source": "saved_state"},
            }
        assert payload["state"] == "active"
        assert payload["module_ref"] == "ConfigCAS:object__module.0#stream:4"
        return {
            "schema": "onec_module_read.v1",
            "status": "ok",
            "text": "Процедура ИзActive()\nКонецПроцедуры\n",
            "origin": {"source": "extension"},
        }

    monkeypatch.setattr(adapter_server, "read_module", fake_read_module)

    result = adapter_server.code_read(
        {
            "base_id": "upo_test",
            "module_ref": "ConfigCASSave:object__module.0#stream:4",
            "state": "both",
        }
    )

    assert len(seen_payloads) == 2
    assert seen_payloads[0]["state"] == "save"
    assert result["status"] == "ok"
    assert result["layers"][0]["source"] == "saved_state"
    assert result["layers"][0]["status"] == "ok"
    assert result["layers"][1]["source"] == "active"
    assert result["layers"][1]["status"] == "ok"
    assert result["comparison"]["both_present"] is True
    assert result["comparison"]["differs"] is True
    assert result["text_source"] == "saved_state"


def test_code_read_state_both_maps_active_form_ref_to_saved_form_ref(monkeypatch: pytest.MonkeyPatch) -> None:
    active_ref = "ConfigCAS:active-form-cas-key#form_module"
    saved_ref = "ConfigCASSave:fb26cf42-7609-11f1-828f-005056b0d483__77494708-43ea-4956-ac3c-199cfb035ad2.0#form_module"
    seen_payloads: list[dict[str, Any]] = []

    monkeypatch.setattr(
        adapter_server,
        "code_read_saved_counterpart_module_ref",
        lambda base_id, module_ref, payload: saved_ref if module_ref == active_ref else None,
    )

    def fake_read_module(payload: dict[str, Any]) -> dict[str, Any]:
        seen_payloads.append(dict(payload))
        if payload.get("state") == "save":
            assert payload["module_ref"] == saved_ref
            return {"schema": "onec_module_read.v1", "status": "ok", "text": "Процедура Тест()\n\t// saved\nКонецПроцедуры\n"}
        assert payload["state"] == "active"
        assert payload["module_ref"] == active_ref
        return {"schema": "onec_module_read.v1", "status": "ok", "text": "Процедура Тест()\n\t// active\nКонецПроцедуры\n"}

    monkeypatch.setattr(adapter_server, "read_module", fake_read_module)

    result = adapter_server.code_read(
        {
            "base_id": "upo_test",
            "kind": "Catalog",
            "name": "test2",
            "form": "t_Форма",
            "module_ref": active_ref,
            "state": "both",
        }
    )

    assert [payload["state"] for payload in seen_payloads] == ["save", "active"]
    assert result["comparison"]["both_present"] is True
    assert result["comparison"]["differs"] is True
    assert result["text_source"] == "saved_state"


def test_code_read_state_both_prefers_direct_active_counterpart_before_owner_selector(monkeypatch: pytest.MonkeyPatch) -> None:
    seen_payloads: list[dict[str, Any]] = []

    def fake_read_module(payload: dict[str, Any]) -> dict[str, Any]:
        seen_payloads.append(dict(payload))
        if payload.get("state") == "save":
            return {"schema": "onec_module_read.v1", "status": "ok", "text": "Процедура ИзSave()\nКонецПроцедуры\n"}
        assert payload["state"] == "active"
        assert payload["module_ref"] == "ConfigCAS:object__module.0#stream:4"
        assert payload["module_id"] == "ConfigCAS:object__module.0#stream:4"
        assert payload["kind"] == "CommonModule"
        assert payload["name"] == "ТестовыйМодуль"
        return {"schema": "onec_module_read.v1", "status": "ok", "text": "Процедура ИзActive()\nКонецПроцедуры\n"}

    monkeypatch.setattr(adapter_server, "read_module", fake_read_module)

    result = adapter_server.code_read(
        {
            "base_id": "upo_test",
            "module_ref": "ConfigCASSave:object__module.0#stream:4",
            "object_type": "CommonModule",
            "object_name": "ТестовыйМодуль",
            "state": "both",
        }
    )

    assert len(seen_payloads) == 2
    assert result["comparison"]["both_present"] is True
    assert result["comparison"]["differs"] is True
    assert result["layers"][0]["text"].startswith("Процедура ИзSave")
    assert result["layers"][1]["text"].startswith("Процедура ИзActive")


def test_code_read_state_both_falls_back_to_logical_owner_when_direct_counterpart_is_absent(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    extension_guid = "11111111-1111-1111-1111-111111111111"
    owner_guid = "22222222-2222-2222-2222-222222222222"
    saved_ref = f"ConfigCASSave:{extension_guid}__module-guid.0#stream:4"
    seen_payloads: list[dict[str, Any]] = []

    def fake_read_module(payload: dict[str, Any]) -> dict[str, Any]:
        seen_payloads.append(dict(payload))
        if payload.get("state") == "save":
            return {
                "schema": "onec_module_read.v1",
                "status": "ok",
                "text": "Процедура ИзSave()\nКонецПроцедуры\n",
                "owner": {
                    "status": "resolved",
                    "kind": "CommonModule",
                    "name": "ТестовыйМодуль",
                    "guid": owner_guid,
                },
                "module": {"kind": "manager_module", "name": "Модуль менеджера"},
            }
        if payload.get("module_ref"):
            assert payload["module_ref"] == f"ConfigCAS:{extension_guid}__module-guid.0#stream:4"
            return {"schema": "onec_module_read.v1", "status": "source_missing", "error": "source_missing"}
        assert payload["table"] == "ConfigCAS"
        assert payload["kind"] == "CommonModule"
        assert payload["name"] == "ТестовыйМодуль"
        assert payload["guid"] == owner_guid
        assert payload["extension_guid"] == extension_guid
        assert payload["module"] == "Модуль менеджера"
        return {
            "schema": "onec_module_read.v1",
            "status": "ok",
            "text": "Процедура ИзActive()\nКонецПроцедуры\n",
        }

    monkeypatch.setattr(adapter_server, "read_module", fake_read_module)

    result = adapter_server.code_read(
        {
            "base_id": "upo_test",
            "module_ref": saved_ref,
            "state": "both",
        }
    )

    assert len(seen_payloads) == 3
    assert result["status"] == "ok"
    assert result["layers"][1]["status"] == "ok"
    assert result["layers"][1]["counterpart_resolution"]["strategy"] == "logical_owner_and_module_role"
    assert result["comparison"]["both_present"] is True
    assert result["comparison"]["differs"] is True


def test_code_read_compare_returns_text_hashes_without_exposing_text(monkeypatch: pytest.MonkeyPatch) -> None:
    def fake_read_module(payload: dict[str, Any]) -> dict[str, Any]:
        text = (
            "Процедура ИзSave()\nКонецПроцедуры\n"
            if payload.get("state") == "save"
            else "Процедура ИзActive()\nКонецПроцедуры\n"
        )
        return {"schema": "onec_module_read.v1", "status": "ok", "text": text}

    monkeypatch.setattr(adapter_server, "read_module", fake_read_module)

    result = adapter_server.code_read(
        {
            "base_id": "upo_test",
            "module_ref": "ConfigCASSave:object__module.0#stream:4",
            "state": "both",
            "include_text": False,
        }
    )

    assert result["comparison"]["both_present"] is True
    assert result["comparison"]["module_text_same"] is False
    assert result["comparison"]["active_text_sha1"]
    assert result["comparison"]["saved_text_sha1"]
    assert "text" not in result
    assert all("text" not in layer for layer in result["layers"])


def test_code_search_preserves_public_ref_in_read_selector(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "search_modules",
        lambda payload: {
            "status": "ok",
            "source": {"kind": "live_metadata"},
            "matches": [
                {
                    "snippet": {"text": "Команда();", "offset": 0},
                    "owner": {"status": "resolved", "kind": "DataProcessor", "name": "ТестоваяОбработка"},
                    "module": {"name": "Модуль объекта", "module_ordinal": 1},
                    "read_selector": {
                        "base_id": "upo_test",
                        "method": "modules.read",
                        "kind": "DataProcessor",
                        "name": "ТестоваяОбработка",
                        "ref": "DataProcessor.ТестоваяОбработка",
                        "module_ordinal": 1,
                    },
                }
            ],
            "counts": {"matches": 1, "complete": True, "scan_limit_hit": False},
            "diagnostics": {},
        },
    )

    result = adapter_server.code_search({"base_id": "upo_test", "query": "Команда", "limit": 10})

    selector = result["items"][0]["read_selector"]
    assert selector["method"] == "code.read"
    assert selector["ref"] == "DataProcessor.ТестоваяОбработка"
    assert selector["name"] == "ТестоваяОбработка"
    assert selector["module_ordinal"] == 1


def test_code_search_prepare_replace_returns_copy_ready_contextual_selector(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "search_modules",
        lambda _payload: {
            "status": "ok",
            "source": {"kind": "live_metadata"},
            "matches": [
                {
                    "snippet": {"text": "Повтор", "offset": 0},
                    "owner": {"status": "resolved", "kind": "Report", "name": "Тест"},
                    "origin": {"extension": {"name": "ТестРасширение"}},
                    "module": {"name": "Модуль объекта", "module_ordinal": 1},
                    "read_selector": {"base_id": "upo_test", "method": "modules.read", "ref": "Report.Тест", "module_ordinal": 1},
                }
            ],
            "counts": {"matches": 1, "complete": True, "scan_limit_hit": False},
            "diagnostics": {},
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "code_read",
        lambda _payload: {"status": "ok", "text": "Начало\nПовтор\nУникальныйКонец", "content_sha1": "a" * 40},
    )

    result = adapter_server.code_search(
        {"base_id": "upo_test", "query": "Повтор", "prepare_replace": True, "anchor_chars": 30}
    )

    candidate = result["items"][0]["replacement_candidates"][0]
    assert candidate["status"] == "ready"
    assert candidate["write_selector"] == {
        "base_id": "upo_test",
        "ref": "Report.Тест",
        "extension": "ТестРасширение",
        "expected_text_sha1": "a" * 40,
    }
    assert candidate["old"] == "Повтор"
    assert result["query"]["prepare_replace"] is True


def test_code_search_accepts_public_ref_selector(monkeypatch: pytest.MonkeyPatch) -> None:
    seen_payloads: list[dict[str, Any]] = []

    def fake_search_modules(payload: dict[str, Any]) -> dict[str, Any]:
        seen_payloads.append(payload)
        return {
            "status": "ok",
            "source": {"kind": "live_metadata"},
            "matches": [],
            "counts": {"matches": 0, "complete": True, "scan_limit_hit": False},
        }

    monkeypatch.setattr(adapter_server, "search_modules", fake_search_modules)

    result = adapter_server.call_method(
        "code.search",
        {"base_id": "upo_test", "query": "ЦелеваяПроцедура", "ref": "Обработка.ТестоваяОбработка"},
    )

    assert result["status"] == "not_found"
    assert seen_payloads[0]["kind"] == "DataProcessor"
    assert seen_payloads[0]["name"] == "ТестоваяОбработка"


def test_code_search_direct_call_accepts_public_ref_selector(monkeypatch: pytest.MonkeyPatch) -> None:
    seen_payloads: list[dict[str, Any]] = []

    def fake_search_modules(payload: dict[str, Any]) -> dict[str, Any]:
        seen_payloads.append(payload)
        return {
            "status": "ok",
            "source": {"kind": "live_metadata"},
            "matches": [],
            "counts": {"matches": 0, "complete": True, "scan_limit_hit": False},
        }

    monkeypatch.setattr(adapter_server, "search_modules", fake_search_modules)

    result = adapter_server.code_search(
        {"base_id": "upo_test", "query": "ЦелеваяПроцедура", "ref": "Обработка.ТестовыйОбъект"}
    )

    assert result["status"] == "not_found"
    assert seen_payloads[0]["kind"] == "DataProcessor"
    assert seen_payloads[0]["name"] == "ТестовыйОбъект"


def test_code_read_direct_call_accepts_public_ref_selector(monkeypatch: pytest.MonkeyPatch) -> None:
    seen_payloads: list[dict[str, Any]] = []

    def fake_read_module(payload: dict[str, Any]) -> dict[str, Any]:
        seen_payloads.append(payload)
        return {"status": "ok", "text": "Процедура Команда()\nКонецПроцедуры\n"}

    monkeypatch.setattr(adapter_server, "read_module", fake_read_module)

    result = adapter_server.code_read({"base_id": "upo_test", "ref": "Обработка.ТестовыйОбъект"})

    assert result["status"] == "ok"
    assert seen_payloads[0]["kind"] == "DataProcessor"
    assert seen_payloads[0]["name"] == "ТестовыйОбъект"


def test_attributes_job_stops_after_card_not_found(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[str] = []

    def fake_call_method_impl(method: str, payload: dict) -> dict:
        calls.append(method)
        return {
            "schema": "onec_adapter_request_error.v1",
            "method": method,
            "status": "not_found",
            "error": "not_found",
            "diagnostics": {"message": "object was not found"},
            "matches": [],
        }

    job_id = "test-attributes-not-found"
    adapter_server.ADAPTER_JOBS[job_id] = {"status": "running", "created_at": adapter_server.adapter_now(), "updated_at": adapter_server.adapter_now()}
    monkeypatch.setattr(adapter_server, "call_method_impl", fake_call_method_impl)

    adapter_server.adapter_run_metadata_object_attributes_job(
        job_id,
        {"base_id": "upo_test", "kind": "Document", "name": "НеСуществующийДокумент", "only": "all"},
        240,
    )

    job = adapter_server.ADAPTER_JOBS[job_id]
    assert calls == ["metadata.object.get"]
    assert job["status"] == "done"
    assert job["result"]["status"] == "not_found"
    assert job["result"]["sections"]["card"] == "failed"
    assert job["result"]["sections"]["semantic"] == "not_started_due_to_card_failure"
    assert job["progress"]["current_step"] == "done"
    adapter_server.ADAPTER_JOBS.pop(job_id, None)


def test_adapter_job_start_returns_not_found_without_job_for_missing_object(monkeypatch: pytest.MonkeyPatch) -> None:
    before_jobs = set(adapter_server.ADAPTER_JOBS)

    def fake_call_method_impl(method: str, payload: dict) -> dict:
        assert method == "metadata.object.get"
        return {
            "schema": "onec_metadata_object.v1",
            "method": "metadata.object.get",
            "status": "not_found",
            "error": "not_found",
            "object": None,
            "matches": [],
            "diagnostics": {"message": "object was not found"},
        }

    monkeypatch.setattr(adapter_server, "call_method_impl", fake_call_method_impl)

    result = adapter_server.adapter_start_job(
        {
            "method": "metadata.object.attributes",
            "payload": {"base_id": "upo_test", "kind": "Document", "name": "НеСуществующийДокумент", "only": "all"},
        }
    )

    assert result["status"] == "not_found"
    assert result["sections"]["card"] == "failed"
    assert result["sections"]["semantic"] == "not_started_due_to_card_failure"
    assert "job_id" not in result
    assert set(adapter_server.ADAPTER_JOBS) == before_jobs


def test_attributes_cancel_marks_running_sections_cancelled() -> None:
    now = adapter_server.adapter_now()
    job_id = "test-attributes-cancel"
    adapter_server.ADAPTER_JOBS[job_id] = {
        "schema": "onec_adapter_job.v1",
        "status": "running",
        "method": "metadata.object.attributes",
        "created_at": now,
        "started_at": now,
        "updated_at": now,
        "partial_result": {
            "schema": "onec_metadata_object_attributes.v1",
            "status": "partial",
            "sections": {
                "card": "ok",
                "semantic": "running",
                "type_resolution": "pending",
                "build_result": "pending",
            },
            "section_timings": {
                "semantic": {"method": "metadata.object.attributes", "status": "running", "started_at": now},
            },
            "diagnostics": [],
        },
    }

    result = adapter_server.adapter_cancel_job({"job_id": job_id})

    assert result["status"] == "cancelled"
    assert result["current_step"] == "cancelled"
    assert result["diagnostics"]["termination"] == "hard_process"
    assert result["partial_result"]["sections"]["semantic"] == "cancelled"
    assert result["partial_result"]["sections"]["type_resolution"] == "cancelled"
    assert result["progress"]["running_steps"] == []
    adapter_server.ADAPTER_JOBS.pop(job_id, None)


def test_adapter_jobs_persist_in_local_sqlite_and_drop_credentials(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    state_db = tmp_path / "adapter-state.sqlite"
    legacy_json = tmp_path / "adapter-jobs.json"
    monkeypatch.setenv("ONEC_ADAPTER_STATE_DB", str(state_db))
    monkeypatch.setenv("ONEC_ADAPTER_JOB_STORE", str(legacy_json))
    original_jobs = dict(adapter_server.ADAPTER_JOBS)
    original_loaded = adapter_server.ADAPTER_JOB_STORE_LOADED
    now = adapter_server.adapter_now()

    try:
        adapter_server.ADAPTER_JOBS.clear()
        adapter_server.ADAPTER_JOB_STORE_LOADED = True
        adapter_server.ADAPTER_JOBS.update(
            {
                "queued-job": {
                    "schema": "onec_adapter_job.v1",
                    "status": "queued",
                    "job_id": "queued-job",
                    "method": "metadata.object.full",
                    "created_at": now,
                    "updated_at": now,
                },
                "done-job": {
                    "schema": "onec_adapter_job.v1",
                    "status": "done",
                    "job_id": "done-job",
                    "method": "metadata.object.get",
                    "created_at": now,
                    "updated_at": now,
                    "result": {"status": "ok", "password": "must-not-be-stored", "password_state": "set"},
                },
            }
        )

        adapter_server.adapter_save_jobs_to_store()
        adapter_server.ADAPTER_JOBS.clear()
        adapter_server.ADAPTER_JOB_STORE_LOADED = False
        adapter_server.adapter_load_jobs_from_store()

        assert state_db.is_file()
        assert not legacy_json.exists()
        assert adapter_server.ADAPTER_JOBS["queued-job"]["status"] == "error"
        assert adapter_server.ADAPTER_JOBS["queued-job"]["error"] == "adapter_restarted"
        assert adapter_server.ADAPTER_JOBS["done-job"]["result"]["status"] == "ok"
        assert adapter_server.ADAPTER_JOBS["done-job"]["result"]["password_state"] == "set"
        assert "password" not in adapter_server.ADAPTER_JOBS["done-job"]["result"]
        with sqlite3.connect(state_db) as conn:
            assert conn.execute("SELECT COUNT(*) FROM adapter_jobs").fetchone()[0] == 2
    finally:
        adapter_server.ADAPTER_JOBS.clear()
        adapter_server.ADAPTER_JOBS.update(original_jobs)
        adapter_server.ADAPTER_JOB_STORE_LOADED = original_loaded


def test_adapter_background_job_runs_in_killable_process(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_STATE_DB", str(tmp_path / "adapter-state.sqlite"))
    monkeypatch.setenv("ONEC_ADAPTER_JOB_PROCESS_ISOLATION", "true")
    monkeypatch.setenv("ONEC_ADAPTER_JOB_TIMEOUT_SECONDS", "20")

    accepted = adapter_server.adapter_start_job({"method": "help.methods", "payload": {}, "timeout_seconds": 20})
    assert accepted["status"] == "accepted"
    assert accepted["timeout_seconds"] == 20
    job_id = accepted["job_id"]
    deadline = time.time() + 20
    job: dict[str, Any] = {}
    try:
        while time.time() < deadline:
            with adapter_server.ADAPTER_JOB_LOCK:
                job = dict(adapter_server.ADAPTER_JOBS.get(job_id) or {})
            if job.get("status") in {"done", "error", "cancelled"}:
                break
            time.sleep(0.05)

        assert job["status"] == "done"
        assert job["worker"]["mode"] == "process"
        assert int(job["worker"]["pid"]) > 0
        assert job["result"]["schema"] == "onec_adapter_methods.v1"
        assert job["result"]["count"] == len(adapter_server.METHODS)
    finally:
        with adapter_server.ADAPTER_JOB_LOCK:
            adapter_server.ADAPTER_JOBS.pop(job_id, None)
            adapter_server.adapter_save_jobs_to_store()


def test_adapter_job_process_timeout_terminates_child(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_STATE_DB", str(tmp_path / "adapter-state.sqlite"))
    job_id = "isolated-timeout"
    now = adapter_server.adapter_now()
    adapter_server.ADAPTER_JOBS[job_id] = {
        "schema": "onec_adapter_job.v1",
        "status": "running",
        "job_id": job_id,
        "method": "help.methods",
        "created_at": now,
        "started_at": now,
        "updated_at": now,
    }
    try:
        adapter_server.adapter_run_job_in_process(job_id, "help.methods", {}, 0.001)

        job = adapter_server.ADAPTER_JOBS[job_id]
        assert job["status"] == "error"
        assert job["error"] == "job_timeout"
        assert "terminated" in job["diagnostics"]["message"]
    finally:
        adapter_server.ADAPTER_JOBS.pop(job_id, None)


def test_definition_find_process_timeout_keeps_streamed_partial_matches(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    class FakeQueue:
        def __init__(self) -> None:
            self.events = [{
                "type": "set",
                "updates": {
                    "partial_result": {
                        "schema": "onec_metadata_definition_find.v1",
                        "status": "partial",
                        "base_id": "upo_test",
                        "matches": [{"area": "modules", "name": "Найдено"}],
                        "counts": {"matches": 1, "by_area": {"modules": 1}},
                    },
                },
            }]

        def get(self, timeout: float | None = None) -> dict[str, Any]:
            if self.events:
                return self.events.pop(0)
            raise adapter_server.queue.Empty()

        def get_nowait(self) -> dict[str, Any]:
            raise adapter_server.queue.Empty()

        def close(self) -> None:
            return None

    class FakeProcess:
        def __init__(self) -> None:
            self.exitcode = None
            self.alive = True

        def start(self) -> None:
            return None

        def is_alive(self) -> bool:
            return self.alive

        def terminate(self) -> None:
            self.alive = False

        def join(self, timeout: float | None = None) -> None:
            return None

    fake_queue = FakeQueue()
    fake_process = FakeProcess()

    class FakeContext:
        def Queue(self) -> FakeQueue:
            return fake_queue

        def Process(self, **kwargs: Any) -> FakeProcess:
            return fake_process

    monkeypatch.setenv("ONEC_ADAPTER_STATE_DB", str(tmp_path / "adapter-state.sqlite"))
    monkeypatch.setattr(adapter_server.multiprocessing, "get_context", lambda *_args, **_kwargs: FakeContext())
    job_id = "definition-find-partial-timeout"
    now = adapter_server.adapter_now()
    adapter_server.ADAPTER_JOBS[job_id] = {
        "schema": "onec_adapter_job.v1",
        "status": "running",
        "job_id": job_id,
        "method": "metadata.definition.find",
        "created_at": now,
        "started_at": now,
        "updated_at": now,
    }
    try:
        adapter_server.adapter_run_job_in_process(job_id, "metadata.definition.find", {"base_id": "upo_test"}, 0.001)

        job = adapter_server.ADAPTER_JOBS[job_id]
        assert job["status"] == "done"
        assert job["result"]["status"] == "partial"
        assert job["result"]["matches"] == [{"area": "modules", "name": "Найдено"}]
        assert job["result"]["counts"]["matches"] == 1
        assert job["result"]["counts"]["by_area"] == {"modules": 1}
        assert job["result"]["diagnostics"][-1]["code"] == "time_budget_exhausted"
    finally:
        adapter_server.ADAPTER_JOBS.pop(job_id, None)


def test_code_index_search_process_timeout_returns_partial(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    class FakeQueue:
        def get(self, timeout: float | None = None) -> dict[str, Any]:
            raise adapter_server.queue.Empty()

        def get_nowait(self) -> dict[str, Any]:
            raise adapter_server.queue.Empty()

        def close(self) -> None:
            return None

    class FakeProcess:
        exitcode = None

        def __init__(self) -> None:
            self.alive = True

        def start(self) -> None:
            return None

        def is_alive(self) -> bool:
            return self.alive

        def terminate(self) -> None:
            self.alive = False

        def join(self, timeout: float | None = None) -> None:
            return None

    class FakeContext:
        def Queue(self) -> FakeQueue:
            return FakeQueue()

        def Process(self, **kwargs: Any) -> FakeProcess:
            return FakeProcess()

    monkeypatch.setenv("ONEC_ADAPTER_STATE_DB", str(tmp_path / "adapter-state.sqlite"))
    monkeypatch.setattr(adapter_server.multiprocessing, "get_context", lambda *_args, **_kwargs: FakeContext())
    job_id = "code-index-partial-timeout"
    now = adapter_server.adapter_now()
    adapter_server.ADAPTER_JOBS[job_id] = {"schema": "onec_adapter_job.v1", "status": "running", "job_id": job_id, "method": "metadata.code_index.search", "created_at": now, "started_at": now, "updated_at": now}
    try:
        adapter_server.adapter_run_job_in_process(job_id, "metadata.code_index.search", {"base_id": "upo_test", "query": "needle"}, 0.001)
        job = adapter_server.ADAPTER_JOBS[job_id]
        assert job["status"] == "done"
        assert job["result"]["schema"] == "onec_code_index_search.v1"
        assert job["result"]["status"] == "partial"
        assert job["result"]["diagnostics"][-1]["code"] == "time_budget_exhausted"
    finally:
        adapter_server.ADAPTER_JOBS.pop(job_id, None)


def test_member_type_evidence_exposes_only_declared_reference_target() -> None:
    evidence = adapter_server.public_member_type_evidence(
        [{"name": "ДокументОснование", "type": {"kind": "reference", "presentation": "ДокументСсылка.РасходнаяНакладная"}}],
        [],
        [],
        [],
    )

    assert evidence == [{
        "member": "ДокументОснование",
        "member_kind": "attribute",
        "declared_type": {"kind": "reference", "presentation": "ДокументСсылка.РасходнаяНакладная"},
        "evidence": "metadata_declared_type",
        "status": "confirmed_target_type",
        "targets": [{
            "kind": "Document",
            "name": "РасходнаяНакладная",
            "ref": "Document.РасходнаяНакладная",
            "presentation": "ДокументСсылка.РасходнаяНакладная",
        }],
    }]


def test_relationship_verify_rejects_target_not_declared_by_member_type(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_attributes",
        lambda payload: {
            "status": "ok",
            "object": {"kind": "Document", "name": "СписаниеЗапасов", "ref": "Document.СписаниеЗапасов"},
            "member_type_evidence": [{
                "member": "ДокументОснование",
                "status": "confirmed_target_type",
                "targets": [{"ref": "Document.РасходнаяНакладная"}],
            }],
        },
    )

    result = adapter_server.metadata_relationship_verify(
        {
            "base_id": "upo_test",
            "ref": "Document.СписаниеЗапасов",
            "member": "ДокументОснование",
            "target_ref": "Document.УОП_ПланированиеМеню",
        }
    )

    assert result["status"] == "not_confirmed"
    assert result["relation_status"] == "declared_target_does_not_match"
    assert result["declared_targets"] == [{"ref": "Document.РасходнаяНакладная"}]
    assert result["diagnostics"][0]["code"] == "target_not_declared_by_member_type"


def test_relationship_find_returns_all_direct_declared_matches(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_attributes",
        lambda payload: {
            "status": "ok",
            "object": {"kind": "Document", "name": "СписаниеЗапасов", "ref": "Document.СписаниеЗапасов"},
            "attributes": [
                {"name": "ДокументОснование", "type": {"kind": "reference", "presentation": "ДокументСсылка.РасходнаяНакладная"}},
                {"name": "СвязанныйДокумент", "type": {"kind": "reference", "presentation": "ДокументСсылка.РасходнаяНакладная"}},
            ],
            "tabular_sections": [],
            "dimensions": [],
            "resources": [],
        },
    )

    result = adapter_server.metadata_relationship_find(
        {"base_id": "upo_test", "ref": "Document.СписаниеЗапасов", "target_ref": "Document.РасходнаяНакладная"}
    )

    assert result["status"] == "confirmed"
    assert [item["member"] for item in result["matches"]] == ["ДокументОснование", "СвязанныйДокумент"]
    assert result["counts"] == {"matches": 2}


def test_relationship_find_suggests_non_confirming_static_followup(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_attributes",
        lambda payload: {
            "status": "ok",
            "object": {"kind": "Document", "name": "СписаниеЗапасов", "ref": "Document.СписаниеЗапасов"},
            "attributes": [], "tabular_sections": [], "dimensions": [], "resources": [],
        },
    )

    result = adapter_server.metadata_relationship_find(
        {"base_id": "upo_test", "ref": "Document.СписаниеЗапасов", "target_ref": "Document.УОП_ПланированиеМеню"}
    )

    assert result["status"] == "not_found"
    assert result["follow_up"][0]["evidence_level"] == "unconfirmed_static_mentions"
    assert result["follow_up"][0]["request"]["query"] == "УОП_ПланированиеМеню"
    assert result["follow_up"][0]["method"] == "metadata.code_index.search"
    assert [item["request"]["areas"] for item in result["follow_up"][1:]] == [["form"], ["templates"]]


def test_attributes_cache_requires_explicit_use_cache(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    seen: dict[str, Any] = {}
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
    config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
    object_guid = "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec"
    cached_payload = {
        "schema": "onec_metadata_object_attributes.v1",
        "status": "ok",
        "base_id": "upo_test",
        "query": {"kind": "Document", "name": "АвансовыйОтчет", "only": "attributes"},
        "object": {"guid": object_guid, "kind": "Document", "name": "АвансовыйОтчет"},
        "dimensions": [],
        "resources": [],
        "attributes": [{"name": "СтарыйКеш"}],
        "tabular_sections": [],
        "counts": {"attributes": 1},
    }
    adapter_server.metadata_guid_index_upsert(
        config,
        {
            "guid": object_guid,
            "guid_role": adapter_server.metadata_attributes_cache_role("attributes"),
            "kind": "Document",
            "kind_ru": "Документ",
            "name": "АвансовыйОтчет",
            "payload": cached_payload,
        },
    )
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
    monkeypatch.setattr(
        adapter_server,
        "get_object",
        lambda *args, **kwargs: {
            "status": "ok",
            "object": {"guid": object_guid, "kind": "Document", "kind_ru": "Документ", "name": "АвансовыйОтчет"},
            "semantic": {"sections": []},
        },
    )
    monkeypatch.setattr(adapter_server, "resolve_type_guids", lambda *args, **kwargs: (seen.update(kwargs), {})[1])
    monkeypatch.setattr(adapter_server, "extension_map_by_guid", lambda base_id: {})

    cached_result = adapter_server.metadata_object_attributes(
        {"base_id": "upo_test", "kind": "Document", "name": "АвансовыйОтчет", "only": "attributes", "use_cache": True}
    )
    live_result = adapter_server.metadata_object_attributes(
        {"base_id": "upo_test", "kind": "Document", "name": "АвансовыйОтчет", "only": "attributes"}
    )

    assert cached_result["attributes"] == [{"name": "СтарыйКеш"}]
    assert cached_result["cache"]["status"] == "hit"
    assert live_result["status"] == "ok"
    assert live_result["attributes"] == []
    assert live_result["cache"]["status"] == "stored"
    assert live_result["query"]["resolve_generated_types"] is False
    assert seen["resolve_generated_live"] is False


def test_payload_lossless_round_trip_envelopes() -> None:
    payload = '{"Имя","Синоним ""с кавычками""",{1,2,3}}'.encode("utf-8-sig")
    for compression in ("none", "raw_deflate", "zlib", "gzip"):
        stored = compress_payload(payload, compression)
        decoded = decode_payload_lossless(stored)
        encoded = encode_payload_lossless(decoded)
        restored, restored_compression = try_decompress(encoded)
        assert restored_compression == compression
        assert restored == payload


def test_noop_policy_can_keep_original_bytes() -> None:
    payload = '{"Имя","Синоним"}'.encode("utf-8-sig")
    stored = compress_payload(payload, "raw_deflate")
    decoded = decode_payload_lossless(stored)
    assert stored == stored
    assert encode_payload_lossless(decoded) != b""


def test_brace_tree_canonical_serialize_and_encode() -> None:
    payload = '{"Имя","Синоним ""с кавычками""",{1,2,3}}'.encode("utf-8-sig")
    stored = compress_payload(payload, "raw_deflate")
    decoded = decode_payload_lossless(stored)
    tree = parse_brace_text(decoded["text"] or "")
    assert serialize_brace_tree(tree) == '{"Имя","Синоним ""с кавычками""",{1,2,3}}'
    encoded = encode_brace_tree(tree, decoded)
    restored, compression = try_decompress(encoded)
    assert compression == "raw_deflate"
    assert restored.decode("utf-8-sig") == '{"Имя","Синоним ""с кавычками""",{1,2,3}}'


def test_set_tree_path_preserves_existing_scalar_kind() -> None:
    tree = parse_brace_text('{"Имя",{1,"Старый заголовок"}}')
    changed = set_tree_path(tree, "1.1", "Новый заголовок")
    changed = set_tree_path(changed, "1.0", 2)

    assert get_tree_path(changed, "1.1")["type"] == "string"
    assert get_tree_path(changed, "1.0")["type"] == "atom"
    assert serialize_brace_tree(changed) == '{"Имя",{2,"Новый заголовок"}}'


def test_patch_brace_text_path_preserves_surrounding_format() -> None:
    text = '\ufeff  {"Имя",  {1, "Старый ""заголовок""", 3}}\n'

    patched, info = patch_brace_text_path(text, "1.1", 'Новый "заголовок"')

    assert patched == '\ufeff  {"Имя",  {1, "Новый ""заголовок""", 3}}\n'
    assert info["old"] == 'Старый "заголовок"'
    assert info["new"] == 'Новый "заголовок"'
    assert info["old_node_type"] == "string"
    assert info["new_node_type"] == "string"


def test_patch_brace_text_path_preserves_atom_kind() -> None:
    patched, info = patch_brace_text_path('{"Имя",{1,2,3}}', "1.1", 42)

    assert patched == '{"Имя",{1,42,3}}'
    assert info["old"] == "2"
    assert info["new"] == "42"
    assert info["old_node_type"] == "atom"


def test_append_brace_text_child_preserves_surrounding_text() -> None:
    patched, info = append_brace_text_child('{"A",{0,2,{"Y",1},{"Z",2}},"B"}', "1", parse_brace_text('{"X",3}'))

    assert patched == '{"A",{0,3,{"Y",1},{"Z",2},{"X",3}},"B"}'
    assert info["parent_path"] == "1"
    assert info["inserted_index"] == 4
    assert info["count_updated"] is True
    assert info["count_before"] == 2
    assert info["count_after"] == 3


def test_append_brace_text_child_keeps_non_declared_container_countless() -> None:
    patched, info = append_brace_text_child('{"A",{1,4},"B"}', "1", parse_brace_text('{"X",3}'))

    assert patched == '{"A",{1,4,{"X",3}},"B"}'
    assert info["count_updated"] is False


def test_decode_config_semantic_from_upo_sample() -> None:
    sample = ROOT / "reports" / "1c-sql" / "upo" / "Config-samples" / "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec"
    decoded = decode_payload_lossless(sample.read_bytes())
    tree = parse_brace_text(decoded["text"] or "")
    identity = find_identity(tree)
    semantic = decode_config_semantic(tree, kind="Document")

    assert identity is not None
    assert identity.guid == "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec"
    assert semantic["identity"]["name"] == "АвансовыйОтчет"
    assert {section["category"] for section in semantic["sections"]} == {"TabularSection", "Attribute"}
    assert semantic["counts"]["generic_record_containers"] > 0
    tabular_section = next(section for section in semantic["sections"] if section["category"] == "TabularSection")
    assert any(record.get("columns") for record in tabular_section["records"])
    first_column = next(column for record in tabular_section["records"] for column in record.get("columns", []) if column.get("type"))
    assert first_column["type"]["kind"] in {"string", "number", "boolean", "date", "reference"}
    attribute_section = next(section for section in semantic["sections"] if section["category"] == "Attribute")
    first_typed_attribute = next(record for record in attribute_section["records"] if record.get("type"))
    assert first_typed_attribute["type"]["kind"] in {"string", "number", "boolean", "date", "reference"}
    assert attribute_section["records"][0]["identity"]["guid"] == "977014d5-3fac-4bdf-9d20-81b437053042"
    assert attribute_section["records"][0]["likely_name"] == "Автор"


def test_decode_config_semantic_can_skip_generic_tree_scan() -> None:
    sample = ROOT / "reports" / "1c-sql" / "upo" / "Config-samples" / "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec"
    decoded = decode_payload_lossless(sample.read_bytes())
    tree = parse_brace_text(decoded["text"] or "")
    semantic = decode_config_semantic(tree, kind="Document", include_generic=False)

    assert semantic["sections"]
    assert semantic["generic_sections"] == []
    assert semantic["generic_record_containers"] == []
    assert semantic["counts"]["generic_record_containers"] == 0


def test_decode_config_semantic_can_limit_section_categories() -> None:
    sample = ROOT / "reports" / "1c-sql" / "upo" / "Config-samples" / "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec"
    decoded = decode_payload_lossless(sample.read_bytes())
    tree = parse_brace_text(decoded["text"] or "")
    semantic = decode_config_semantic(tree, kind="Document", include_generic=False, categories={"Attribute"})

    assert {section["category"] for section in semantic["sections"]} == {"Attribute"}
    assert [rule["category"] for rule in semantic["section_rules"]] == ["Attribute"]


def test_decode_config_semantic_lightweight_keeps_public_fields() -> None:
    sample = ROOT / "reports" / "1c-sql" / "upo" / "Config-samples" / "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec"
    decoded = decode_payload_lossless(sample.read_bytes())
    tree = parse_brace_text(decoded["text"] or "")
    semantic = decode_config_semantic(
        tree,
        kind="Document",
        include_generic=False,
        categories={"Attribute"},
        lightweight=True,
    )

    attribute_section = semantic["sections"][0]
    first_attribute = attribute_section["records"][0]
    assert first_attribute["identity"]["name"] == "Автор"
    assert first_attribute["type"]
    assert "strings_sample" not in first_attribute
    assert "guids_sample" not in first_attribute


def test_public_storage_trace_scrubber_hides_sql_coordinates() -> None:
    payload = {
        "status": "source_missing",
        "source": {"kind": "live_sql", "database": "upo_test", "table": "Config", "file_name": "abc"},
        "object": {"name": "Документ", "storage_routes": [{"file_name": "abc"}]},
        "modules": [
            {
                "name": "Config:abc#stream:0",
                "module_id": "Config:abc#stream:0",
                "source": {"kind": "live_sql", "database": "upo_test"},
                "payload": {"payload_bytes": 10, "sha1": "1"},
            }
        ],
    }

    public = strip_storage_traces(payload)
    text = str(public)

    assert public["source"] == {"kind": "live_metadata"}
    assert "Документ" in text
    for marker in ("live_sql", "database", "file_name", "module_id", "payload_bytes", "sha1", "storage_routes"):
        assert marker not in text


def test_public_module_and_form_rows_hide_internal_handles() -> None:
    module = public_module_row(
        {
            "module_id": "Config:abc#stream:0",
            "source": {"kind": "live_sql", "database": "upo_test"},
            "file_name": "abc",
            "stream_index": 0,
            "payload_role": "bsl_module_payload",
            "sha1": "1",
            "text_preview": "Процедура Команда()\nКонецПроцедуры",
        },
        ordinal=2,
    )
    form = public_form_row(
        {
            "name": "ФормаДокумента",
            "source": {"kind": "live_sql", "database": "upo_test"},
            "parts": [{"part_id": "abc.0", "source": {"kind": "live_sql"}, "suffix": ".0", "sha1": "1", "role": "form_payload"}],
        }
    )

    assert module["name"] == "Модуль объекта 2"
    assert "module_id" not in module
    assert "source" not in module
    assert "file_name" not in module
    assert form["parts"] == [{"role": "form_payload"}]


def test_classify_stream_payload_with_bsl_marker() -> None:
    module_text = "Процедура Команда1(Команда)\r\nКонецПроцедуры\r\n".encode("utf-8-sig")
    header = f"\r\n{len(module_text):08x} {len(module_text):08x} 7fffffff \r\n".encode("ascii")
    stored = compress_payload(b"prefix" + header + module_text, "raw_deflate")
    classified = classify_payload(stored)

    assert classified["role"] == "bsl_module_payload"
    assert classified["counts"]["stream_blocks"] == 1
    assert classified["stream_blocks"][0]["has_bsl_marker"] is True


def test_replace_stream_block_updates_header_and_text() -> None:
    module_text_raw = "Процедура Команда1(Команда)\r\nКонецПроцедуры\r\n"
    module_text = module_text_raw.encode("utf-8-sig")
    header = f"\r\n{len(module_text):08x} {len(module_text):08x} 7fffffff \r\n".encode("ascii")
    payload = b"prefix" + header + module_text + b"tail"
    expected_text_sha1 = adapter_server.code_text_sha1(module_text_raw)

    changed_payload, edit = replace_stream_block(
        payload,
        0,
        replace={"old": "Команда1", "new": "Команда2"},
        expected_contains="Процедура Команда1",
        expected_text_sha1=expected_text_sha1,
    )
    stored = compress_payload(changed_payload, "raw_deflate")
    classified = classify_payload(stored, include_text=True)

    assert edit["old_sha1"] != edit["new_sha1"]
    assert edit["old_text_sha1"] == expected_text_sha1
    assert edit["new_text_sha1"] == adapter_server.code_text_sha1(classified["stream_blocks"][0]["text"])
    assert classified["role"] == "bsl_module_payload"
    assert "Процедура Команда2" in classified["stream_blocks"][0]["text"]

    with pytest.raises(ValueError, match="expected_text_sha1"):
        replace_stream_block(
            payload,
            0,
            text=module_text_raw,
            expected_text_sha1="0" * 40,
        )


def test_replace_stream_block_accepts_lf_fragment_from_code_read_and_preserves_crlf() -> None:
    module_text_raw = "Процедура Команда1()\r\n\tСтарое();\r\nКонецПроцедуры\r\n"
    encoded = module_text_raw.encode("utf-8-sig")
    header = f"\r\n{len(encoded):08x} {len(encoded):08x} 7fffffff \r\n".encode("ascii")
    payload = b"prefix" + header + encoded

    changed_payload, _edit = replace_stream_block(
        payload,
        0,
        replace={
            "old": "Процедура Команда1()\n\tСтарое();\nКонецПроцедуры\n",
            "new": "Процедура Команда1()\n\tНовое();\nКонецПроцедуры\n",
        },
    )
    text = classify_payload(compress_payload(changed_payload, "raw_deflate"), include_text=True)["stream_blocks"][0]["text"]

    assert "\tНовое();\r\n" in text
    assert "Старое" not in text
    assert "\n" not in text.replace("\r\n", "")


def test_replace_stream_block_routine_edit() -> None:
    module_text = (
        "Процедура Команда1(Команда)\r\n"
        "\tСообщить(\"old\");\r\n"
        "КонецПроцедуры\r\n"
    ).encode("utf-8-sig")
    header = f"\r\n{len(module_text):08x} {len(module_text):08x} 7fffffff \r\n".encode("ascii")
    payload = b"prefix" + header + module_text
    new_routine = "Процедура Команда1(Команда)\r\n\tСообщить(\"new\");\r\nКонецПроцедуры\r\n"

    changed_payload, edit = replace_stream_block(payload, 0, routine={"operation": "replace", "name": "Команда1", "text": new_routine})
    stored = compress_payload(changed_payload, "raw_deflate")
    classified = classify_payload(stored, include_text=True)
    text = classified["stream_blocks"][0]["text"]

    assert edit["routine"]["status"] == "replaced"
    assert 'Сообщить("new")' in text
    assert 'Сообщить("old")' not in text
    assert validate_bsl_text(text)["status"] == "ok"


def test_replace_stream_block_routine_preconditions() -> None:
    module_text_raw = (
        "Процедура Команда1(Команда)\r\n"
        "\tСообщить(\"old\");\r\n"
        "КонецПроцедуры\r\n"
    )
    module_text = module_text_raw.encode("utf-8-sig")
    header = f"\r\n{len(module_text):08x} {len(module_text):08x} 7fffffff \r\n".encode("ascii")
    payload = b"prefix" + header + module_text
    new_routine = "Процедура Команда1(Команда)\r\n\tСообщить(\"new\");\r\nКонецПроцедуры\r\n"

    changed_payload, edit = replace_stream_block(
        payload,
        0,
        routine={
            "operation": "replace",
            "name": "Команда1",
            "text": new_routine,
            "expected_old_contains": 'Сообщить("old")',
            "expected_old_sha1": text_sha1(module_text_raw.rstrip("\r\n")),
        },
    )
    classified = classify_payload(compress_payload(changed_payload, "raw_deflate"), include_text=True)

    assert edit["routine"]["old_sha1"] == text_sha1(module_text_raw.rstrip("\r\n"))
    assert 'Сообщить("new")' in classified["stream_blocks"][0]["text"]

    with pytest.raises(ValueError, match="expected_old_contains"):
        replace_stream_block(
            payload,
            0,
            routine={
                "operation": "replace",
                "name": "Команда1",
                "text": new_routine,
                "expected_old_contains": "missing old marker",
            },
        )

    with pytest.raises(ValueError, match="expected_old_sha1"):
        replace_stream_block(
            payload,
            0,
            routine={
                "operation": "replace",
                "name": "Команда1",
                "text": new_routine,
                "expected_old_sha1": "0" * 40,
            },
        )


def test_validate_bsl_text_routine_balance() -> None:
    ok = validate_bsl_text("Процедура Команда1()\nКонецПроцедуры\n")
    async_ok = validate_bsl_text("Асинх Процедура Команда1()\nКонецПроцедуры\n")
    broken = validate_bsl_text("Процедура Команда1()\n")

    assert ok["status"] == "ok"
    assert async_ok["status"] == "ok"
    assert async_ok["routines_sample"][0]["name"] == "Команда1"
    assert broken["status"] == "error"
    assert broken["issues"][0]["code"] == "routine_balance"


def test_routine_blocks_ignore_comment_mentions() -> None:
    text = (
        "// Процедура отмены / проведения у подчиненных счет-фактур.\n"
        "Процедура ОбработкаПодчиненныхСчетФактур()\n"
        "\tСообщить(\"ok\");\n"
        "КонецПроцедуры\n"
        "\n"
        "// Процедура формирует счет-фактуры для покупателей.\n"
        "Процедура СформироватьСчетаФактуры()\n"
        "КонецПроцедуры\n"
    )

    routines = routine_blocks(text)

    assert [routine["name"] for routine in routines] == [
        "ОбработкаПодчиненныхСчетФактур",
        "СформироватьСчетаФактуры",
    ]


def test_module_text_response_reports_routine_match_mode() -> None:
    text = "Процедура ОбработкаПодчиненныхСчетФактур()\nКонецПроцедуры\n"

    exact = module_text_response(text, {"routine_name": "ОбработкаПодчиненныхСчетФактур", "max_chars": 100})
    case_insensitive = module_text_response(text, {"routine_name": "обработкаподчиненныхсчетфактур", "max_chars": 100})

    assert exact["selection"]["match_by"] == "routine_exact"
    assert case_insensitive["selection"]["match_by"] == "routine_case_insensitive"


def test_module_text_response_is_compact_for_selected_routine_by_default() -> None:
    text = (
        "Процедура Первая()\nКонецПроцедуры\n\n"
        "Процедура Нужная()\n\tСообщить(\"ok\");\nКонецПроцедуры\n"
    )

    compact = module_text_response(text, {"routine_name": "Нужная", "max_chars": 1000})
    expanded = module_text_response(
        text,
        {"routine_name": "Нужная", "max_chars": 1000, "include_routines": True, "include_summary": True},
    )

    assert compact["text"].startswith("Процедура Нужная")
    assert "routines" not in compact
    assert "summary" not in compact
    assert len(expanded["routines"]) == 2
    assert expanded["summary"]["routines"] == 2


def test_modules_read_bare_common_module_name_returns_ready_public_selector(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "get_object",
        lambda *args, **kwargs: {
            "status": "ok",
            "object": {"kind": "CommonModule", "name": "ОтчетыСервер", "ref": "CommonModule.ОтчетыСервер"},
        },
    )

    result = adapter_server.read_module({"base_id": "upo_test", "module_ref": "ОтчетыСервер"})

    assert result["status"] == "error"
    assert result["error"] == "invalid_module_id"
    assert result["diagnostics"]["detected_public_module"]["ref"] == "CommonModule.ОтчетыСервер"
    assert result["diagnostics"]["next_call"] == {
        "method": "modules.read",
        "params": {"base_id": "upo_test", "ref": "CommonModule.ОтчетыСервер", "module_ordinal": 1},
    }


def test_diagnostics_call_chain_resolves_local_and_common_module_calls(monkeypatch: pytest.MonkeyPatch) -> None:
    texts = {
        ("Catalog.Тест", "Старт"): "Процедура Старт()\n\tЛокальный();\n\tОбщий.Метод();\nКонецПроцедуры",
        ("Catalog.Тест", "Локальный"): "Процедура Локальный()\nКонецПроцедуры",
        ("CommonModule.Общий", "Метод"): "Процедура Метод()\nКонецПроцедуры",
    }

    def fake_code_read(payload: dict[str, Any]) -> dict[str, Any]:
        ref = str(payload.get("ref") or "")
        routine = str(payload.get("routine_name") or "")
        text = texts.get((ref, routine))
        if text is None:
            return {"status": "not_found", "selection": {"status": "not_found"}}
        return {
            "status": "ok",
            "text": text,
            "selection": {"routine_name": routine, "line_start": 10, "line_end": 10 + len(text.splitlines()) - 1},
        }

    monkeypatch.setattr(adapter_server, "code_read", fake_code_read)
    monkeypatch.setattr(adapter_server, "metadata_resolve_overrides", lambda payload: {"status": "not_found", "chain": []})

    result = adapter_server.diagnostics_call_chain(
        {"base_id": "upo_test", "ref": "Catalog.Тест", "entry_method": "Старт", "depth": 2}
    )

    assert result["status"] == "ok"
    assert result["schema"] == "onec_diagnostics_call_chain.v2"
    assert {node["routine_name"] for node in result["nodes"]} == {"Старт", "Локальный", "Метод"}
    assert len([edge for edge in result["edges"] if edge["status"] == "resolved"]) == 2
    assert result["counts"]["unresolved_calls"] == 0


def test_modules_read_falls_back_to_single_bsl_stream_in_saved_command_payload(monkeypatch: pytest.MonkeyPatch) -> None:
    class FakeStreamClassifier:
        pass

    bsl_text = "&НаКлиенте\nПроцедура ОбработкаКоманды()\nКонецПроцедуры\n"
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
    monkeypatch.setattr(adapter_server, "metadata_form_owner_cache_lookup", lambda *args, **kwargs: None)
    monkeypatch.setattr(adapter_server, "cached_module_owner_payload", lambda *args, **kwargs: None)
    monkeypatch.setattr(adapter_server, "extension_module_owner_payload", lambda *args, **kwargs: None)
    monkeypatch.setattr(adapter_server, "saved_state_public_module_context", lambda **kwargs: None)
    monkeypatch.setattr(adapter_server, "saved_state_module_owner_identity", lambda **kwargs: None)
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (b"compressed", {"database": "upo_test"}, None))
    monkeypatch.setattr(adapter_server, "payload_text_from_bytes", lambda data: {"status": "undecodable", "text": ""})
    monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
    FakeStreamClassifier.classify_payload = staticmethod(
        lambda data, include_text=True: {
            "role": "bsl_module_payload",
            "stream_blocks": [
                {"text": "descriptor", "has_bsl_marker": False},
                {"text": bsl_text, "has_bsl_marker": True},
            ],
        }
    )

    result = adapter_server.read_module(
        {
            "base_id": "upo_test",
            "kind": "CalculationRegister",
            "module_ref": "ConfigCASSave:extension__command.2",
        }
    )

    assert result["status"] == "ok"
    assert result["text"] == bsl_text
    assert result["module"]["kind"] == "command_module"


def test_modules_read_returns_owner_from_object_selector(monkeypatch: pytest.MonkeyPatch) -> None:
    module_ref = "ConfigCAS:module-file"

    monkeypatch.setattr(
        adapter_server,
        "metadata_object_modules",
        lambda payload: {
            "status": "ok",
            "object": {
                "kind": "Document",
                "name": "ЗаказПокупателя",
                "synonym": "Заказ покупателя",
                "guid": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec",
            },
            "modules": [{"module_id": module_ref}],
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda *args, **kwargs: (b"payload", {"database": "upo_test"}, None),
    )
    monkeypatch.setattr(
        adapter_server,
        "payload_text_from_bytes",
        lambda data: {"status": "ok", "text": "Процедура Команда()\nКонецПроцедуры\n"},
    )

    result = adapter_server.read_module(
        {
            "base_id": "upo_test",
            "kind": "Document",
            "name": "ЗаказПокупателя",
            "module_ordinal": 1,
            "summary": True,
        }
    )

    assert result["status"] == "ok"
    assert result["owner"]["status"] == "resolved"
    assert result["owner"]["kind"] == "Document"
    assert result["owner"]["name"] == "ЗаказПокупателя"
    assert result["module"]["module_ordinal"] == 1


def test_modules_read_direct_call_accepts_public_ref_selector(monkeypatch: pytest.MonkeyPatch) -> None:
    module_ref = "ConfigCAS:module-file"
    seen_payloads: list[dict[str, Any]] = []

    def fake_metadata_object_modules(payload: dict[str, Any]) -> dict[str, Any]:
        seen_payloads.append(payload)
        return {
            "status": "ok",
            "object": {
                "kind": "DataProcessor",
                "name": "ТестовыйОбъект",
                "synonym": "Тестовый объект",
                "guid": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec",
            },
            "modules": [{"module_id": module_ref}],
        }

    monkeypatch.setattr(adapter_server, "metadata_object_modules", fake_metadata_object_modules)
    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda *args, **kwargs: (b"payload", {"database": "upo_test"}, None),
    )
    monkeypatch.setattr(
        adapter_server,
        "payload_text_from_bytes",
        lambda data: {"status": "ok", "text": "Процедура Команда()\nКонецПроцедуры\n"},
    )

    result = adapter_server.read_module(
        {
            "base_id": "upo_test",
            "ref": "Обработка.ТестовыйОбъект",
            "module_ordinal": 1,
            "summary": True,
        }
    )

    assert result["status"] == "ok"
    assert seen_payloads[0]["kind"] == "DataProcessor"
    assert seen_payloads[0]["name"] == "ТестовыйОбъект"
    assert result["owner"]["kind"] == "DataProcessor"
    assert result["owner"]["name"] == "ТестовыйОбъект"


def test_modules_read_returns_owner_from_module_owner_cache(monkeypatch: pytest.MonkeyPatch) -> None:
    module_ref = "ConfigCAS:extension-module#stream:0"

    monkeypatch.setattr(
        adapter_server,
        "sql_config_for_base",
        lambda base_id: ({"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}, None),
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_module_owner_cache_lookup",
        lambda config, ref: {
            "owner": {
                "kind": "Catalog",
                "name": "фс_НоменклатураРасширение",
                "synonym": "Номенклатура расширение",
                "guid": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec",
            },
            "module_payload": {"module_name": "Модуль объекта", "module_ordinal": 1, "stream_index": 0},
        }
        if ref == module_ref
        else None,
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda *args, **kwargs: (b"payload", {"database": "upo_test"}, None),
    )

    class FakeStreamClassifier:
        pass

    monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
    FakeStreamClassifier.classify_payload = staticmethod(
        lambda data, include_text=True: {
            "role": "configcas",
            "stream_blocks": [{"text": "Процедура Команда()\nКонецПроцедуры\n"}],
        }
    )

    result = adapter_server.read_module({"base_id": "upo_test", "module_ref": module_ref, "summary": True})

    assert result["status"] == "ok"
    assert result["owner"]["source"] == "metadata.module_owner_cache"
    assert result["owner"]["name"] == "фс_НоменклатураРасширение"
    assert result["module"]["name"] == "Модуль объекта"
    assert result["module"]["module_ordinal"] == 1
    assert result["module"]["stream_index"] == 0


def test_modules_search_form_payload_returns_container_bsl_offset(monkeypatch: pytest.MonkeyPatch) -> None:
    form_file = "form-payload-file"
    container_prefix = "{4,{0},\""
    form_text = (
        container_prefix
        + "\r\n&НаКлиенте\r\nПроцедура ЗаменаДомена(Команда)\r\n\t// body\r\nКонецПроцедуры\r\n"
        + "\"}"
    )
    expected_offset = form_text.index("&НаКлиенте")

    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {"status": "ok", "files": [{"FileName": form_file}], "counts": {"files": 1}},
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_files_bytes",
        lambda *args, **kwargs: ({form_file: b"payload"}, {"database": "upo_test"}, None),
    )
    monkeypatch.setattr(
        adapter_server,
        "payload_text_from_bytes",
        lambda data: {"status": "ok", "text": form_text},
    )

    result = adapter_server.search_modules(
        {
            "base_id": "upo_test",
            "query": "ЗаменаДомена",
            "scope": "configcas",
            "scan_limit": 10,
            "include_storage": False,
            "state": "active",
        }
    )

    assert result["status"] == "ok"
    match = result["matches"][0]
    assert match["read_selector"]["module_ref"] == f"ConfigCAS:{form_file}"
    assert match["read_selector"]["bsl_offset"] == expected_offset
    assert match["extraction"]["container_offset"] == expected_offset


def test_modules_read_form_module_ref_uses_container_bsl_offset(monkeypatch: pytest.MonkeyPatch) -> None:
    form_file = "form-payload-file"
    form_text = (
        "{4,{0},\""
        "\r\n&НаКлиенте\r\nПроцедура ЗаменаДомена(Команда)\r\n\t// body\r\nКонецПроцедуры\r\n"
        "\"}"
    )
    bsl_offset = form_text.index("&НаКлиенте")

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda *args, **kwargs: (b"payload", {"database": "upo_test"}, None),
    )
    monkeypatch.setattr(
        adapter_server,
        "payload_text_from_bytes",
        lambda data: {"status": "ok", "text": form_text},
    )

    result = adapter_server.read_module(
        {
            "base_id": "upo_test",
            "module_ref": f"ConfigCAS:{form_file}#form_module",
            "bsl_offset": bsl_offset,
            "routine_name": "ЗаменаДомена",
            "include_line_numbers": True,
        }
    )

    assert result["status"] == "ok"
    assert result["selection"]["routine_name"] == "ЗаменаДомена"
    assert result["text"].startswith("Процедура ЗаменаДомена(Команда)")
    assert "// body" in result["text"]
    assert result["extraction"]["bsl_offset"] == bsl_offset


def test_modules_read_skips_extension_owner_scan_for_saved_state_stream_ref(monkeypatch: pytest.MonkeyPatch) -> None:
    module_ref = "ConfigCASSave:object__module.0#stream:4"

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
    monkeypatch.setattr(
        adapter_server,
        "extension_definition_guid_sources",
        lambda *args, **kwargs: pytest.fail("saved-state module refs must not scan extension definitions"),
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda *args, **kwargs: (b"payload", {"database": "upo_test"}, None),
    )

    class FakeStreamClassifier:
        pass

    streams = [{"text": ""} for _ in range(5)]
    streams[4] = {"text": "Процедура Команда()\nКонецПроцедуры\n"}
    monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
    FakeStreamClassifier.classify_payload = staticmethod(
        lambda data, include_text=True: {
            "role": "bsl_module_payload",
            "stream_blocks": streams,
        }
    )

    result = adapter_server.read_module({"base_id": "upo_test", "module_ref": module_ref, "summary": True})

    assert result["status"] == "ok"
    assert result["owner"]["source"] == "direct_saved_state_module_ref"
    assert result["origin"]["source"] == "saved_state"
    assert result["module"]["stream_index"] == 4


def test_code_read_runtime_module_ref_skips_owner_scan_by_default(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_read_module(payload: dict[str, Any]) -> dict[str, Any]:
        seen.update(payload)
        return {"status": "ok", "text": "Процедура Тест()\nКонецПроцедуры", "module_id": payload["module_ref"]}

    monkeypatch.setattr(adapter_server, "read_module", fake_read_module)
    monkeypatch.setattr(
        adapter_server,
        "enrich_code_read_logical_owner",
        lambda *args, **kwargs: pytest.fail("direct runtime module_ref must not run logical-owner resolution"),
    )

    result = adapter_server.code_read(
        {
            "base_id": "upo_test",
            "module_ref": "ConfigCAS:module-file#stream:1",
            "configuration_view": "runtime_applied",
        }
    )

    assert result["status"] == "ok"
    assert seen["resolve_owner"] is False


def test_modules_read_resolves_extension_owner_for_configcas_stream_ref(monkeypatch: pytest.MonkeyPatch) -> None:
    module_ref = "ConfigCAS:12d98fe77f46c4310958c30f7fe87ef8abaf3d32#stream:4"
    owner_guid = "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec"
    extension_guid = "11111111-2222-3333-4444-555555555555"

    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
    monkeypatch.setattr(
        adapter_server,
        "extension_definition_guid_sources",
        lambda base_id, *, timeout_seconds=60: (
            {
                owner_guid: [
                    {
                        "extension": {"guid": extension_guid, "name": "фс_ДоработкиОбщее", "active": True},
                        "storage_role": "Catalog",
                        "sql_number": 12,
                    }
                ]
            },
            None,
        ),
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_modules",
        lambda payload: {
            "status": "ok",
            "object": {
                "kind": "Catalog",
                "name": "РасширенныйСправочник",
                "synonym": "Расширенный справочник",
                "guid": owner_guid,
            },
            "modules": [{"module_id": module_ref}],
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda *args, **kwargs: (b"payload", {"database": "upo_test"}, None),
    )

    class FakeStreamClassifier:
        pass

    streams = [{"text": ""} for _ in range(5)]
    streams[4] = {"text": "Процедура Команда()\nКонецПроцедуры\n"}
    monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
    FakeStreamClassifier.classify_payload = staticmethod(
        lambda data, include_text=True: {
            "role": "configcas",
            "stream_blocks": streams,
        }
    )

    result = adapter_server.read_module({"base_id": "upo_test", "module_ref": module_ref, "summary": True})

    assert result["status"] == "ok"
    assert result["owner"]["source"] == "extension_definition_guid_sources"
    assert result["owner"]["name"] == "РасширенныйСправочник"
    assert result["origin"]["source"] == "extension"
    assert result["origin"]["extension"]["name"] == "фс_ДоработкиОбщее"
    assert result["module"]["module_ordinal"] == 1
    assert result["module"]["stream_index"] == 4


def test_mcp_modules_read_does_not_resolve_owner_itself(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_mcp,
        "metadata_cache_lookup_identity",
        lambda *args, **kwargs: pytest.fail("MCP must not resolve modules.read owner itself"),
    )

    result = adapter_mcp.enrich_modules_read_result(
        {"base_id": "upo_test", "guid": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec"},
        {"status": "ok", "module": {"name": "Модуль объекта"}},
    )

    assert "owner" not in result
    assert "owner_display" not in result


def test_mcp_onec_request_schema_stays_generic() -> None:
    onec_request = next(tool for tool in adapter_mcp.TOOLS if tool["name"] == "onec_request")
    schema = onec_request["inputSchema"]

    assert schema["required"] == ["method"]
    assert "oneOf" not in schema
    assert schema["properties"]["method"]["type"] == "string"
    assert schema["properties"]["payload"]["additionalProperties"] is True


def test_mcp_onec_request_examples_are_generic_and_selector_oriented() -> None:
    onec_request = next(tool for tool in adapter_mcp.TOOLS if tool["name"] == "onec_request")
    schema = onec_request["inputSchema"]
    examples_json = json.dumps(schema.get("examples") or [], ensure_ascii=False)

    for concrete_value in mcp_adapter_contract.FORBIDDEN_CONCRETE_SELECTOR_VALUES:
        assert concrete_value not in examples_json
    assert "<metadata-kind>" in examples_json
    assert "<metadata-object-name>" in examples_json
    assert "<metadata-kind>.<metadata-object-name>" in examples_json
    assert "extension.objects.find" in examples_json
    assert "<extension-name>" in examples_json
    assert "<routine-name>" in examples_json
    assert "<selector-token-from-code-search>" in examples_json
    assert "module_ref" not in examples_json
    assert "owner_scan_limit" in examples_json
    assert '"source_state": "working"' in examples_json
    assert "code.write" in examples_json
    assert "metadata.write" in examples_json
    assert "metadata.form.command_button.write" in examples_json
    assert "metadata.form.command_button.verify" in examples_json
    assert "ОбщаяФорма.<form-name>.<routine-name>" in examples_json
    assert "handler_routine_text" in examples_json
    assert "include_handler" in examples_json
    assert "routine_text" in examples_json
    assert "read_selector.method" in onec_request["description"]
    assert "diagnostics.owner_resolution" in onec_request["description"]
    assert "source_state=working" in onec_request["description"]
    assert "prefer high-level metadata.write" in onec_request["description"]
    assert "metadata.form.command_button.write" in onec_request["description"]
    assert "metadata.form.command_button.verify" in schema["properties"]["method"]["description"]
    assert "code.write" in onec_request["description"]
    assert "ref, kind/name, or object_type/object_name" in onec_request["description"]

    examples = {
        example["method"]: example["payload"]
        for example in schema.get("examples") or []
        if isinstance(example, dict) and isinstance(example.get("payload"), dict)
    }
    prepare = examples["metadata.saved_state.prepare"]
    assert prepare["layer"] == "extension_saved_state"
    assert "ref" in prepare
    assert not {"target_table", "table", "file_name"} & prepare.keys()
    diff = examples["metadata.saved_state.diff"]
    assert "ref" in diff
    assert "module_ordinal" in diff
    assert not {"target_table", "table", "file_name", "module_ref"} & diff.keys()
    status = examples["metadata.saved_state.status"]
    assert status["layer"] == "extension_saved_state"
    assert not {"target_table", "table"} & status.keys()
    changes = examples["metadata.saved_state.changes.list"]
    assert changes["layer"] == "extension_saved_state"
    payload_description = schema["properties"]["payload"]["description"]
    assert "ref" in payload_description
    assert "kind/name/guid" in payload_description
    assert "object_type/object_name/object_guid" in payload_description


def test_mcp_source_state_maps_to_adapter_state_for_working_methods() -> None:
    working = adapter_mcp.apply_freshness_request_policy(
        {"base_id": "upo_test", "source_mode": "designer", "source_state": "working"},
        "modules.search",
    )
    applied = adapter_mcp.apply_freshness_request_policy(
        {"base_id": "upo_test", "source_state": "applied"},
        "code.search",
    )
    both = adapter_mcp.apply_freshness_request_policy(
        {"base_id": "upo_test", "source_state": "all"},
        "extension.objects.find",
    )
    explicit = adapter_mcp.apply_freshness_request_policy(
        {"base_id": "upo_test", "source_state": "all", "state": "save"},
        "metadata.resolve_overrides",
    )

    assert working["state"] == "working"
    assert applied["state"] == "active"
    assert both["state"] == "both"
    assert explicit["state"] == "save"


def test_mcp_storage_methods_are_reported_as_direct_fresh_reads() -> None:
    transformed = adapter_mcp.apply_freshness_request_policy(
        {"base_id": "upo_test", "diagnostic": True},
        "storage.saved_state.backups.prune",
    )
    enriched = adapter_mcp.enrich_result_with_freshness(
        {"base_id": "upo_test", "diagnostic": True},
        "storage.saved_state.backups.prune",
        {"status": "ok", "dry_run": True},
        time.time(),
    )

    assert transformed["cache_policy"] == "none"
    assert transformed["force_refresh"] is True
    assert enriched["_freshness"]["status"] == "fresh"
    assert enriched["_freshness"]["cache_policy"] == "none"


def test_mcp_code_write_uses_working_saved_state_without_agent_sql_gates() -> None:
    transformed = adapter_mcp.apply_freshness_request_policy(
        {
            "base_id": "upo_test",
            "object_type": "CommonForm",
            "object_name": "t_Форма",
            "routine_name": "ЗаменаДомена",
            "routine_text": "Процедура ЗаменаДомена(Команда)\nКонецПроцедуры",
        },
        "code.write",
    )

    assert transformed["source_mode"] == "designer"
    assert transformed["source_state"] == "working"
    assert transformed["cache_policy"] == "none"
    assert "allow_sql_saved_state_apply" not in transformed
    assert "allow_saved_state_write" not in transformed


def test_mcp_blocks_metadata_write_bsl_and_suggests_code_write() -> None:
    result = adapter_mcp.run_or_enqueue_adapter_method(
        "metadata.write",
        {
            "base_id": "upo_test",
            "target": {"kind": "module", "object_type": "CommonForm", "object_name": "t_Форма"},
            "routine_name": "ЗаменаДомена",
            "routine_text": "Процедура ЗаменаДомена(Команда)\nКонецПроцедуры",
            "mode": "apply",
        },
    )

    assert result["status"] == "blocked"
    assert result["reason"] == "use_code_write_for_bsl"
    suggested = result["diagnostics"]["suggested_request"]
    assert suggested["method"] == "code.write"
    assert suggested["payload"]["object_type"] == "CommonForm"
    assert suggested["payload"]["object_name"] == "t_Форма"
    assert suggested["payload"]["routine_name"] == "ЗаменаДомена"
    assert suggested["payload"]["mode"] == "apply"
    assert "allow_sql_saved_state_apply" not in suggested["payload"]
    assert "allow_saved_state_write" not in suggested["payload"]


def test_mcp_code_write_suggestion_does_not_treat_module_as_1c_owner_type() -> None:
    result = adapter_mcp.run_or_enqueue_adapter_method(
        "metadata.module.write_apply",
        {
            "base_id": "upo_test",
            "target": {
                "kind": "module",
                "name": "Модуль объекта",
                "module_ref": "ConfigCASSave:object__module.0#stream:4",
            },
            "module_text": "Процедура Тест()\nКонецПроцедуры\n",
            "mode": "apply",
        },
    )

    suggested_payload = result["diagnostics"]["suggested_request"]["payload"]
    assert result["reason"] == "use_code_write_for_bsl"
    assert suggested_payload["module_ref"] == "ConfigCASSave:object__module.0#stream:4"
    assert "object_type" not in suggested_payload
    assert "object_name" not in suggested_payload
    assert "object_guid" not in suggested_payload


def test_mcp_source_does_not_advertise_extension_filter_for_metadata_objects_list() -> None:
    source = Path(adapter_mcp.__file__).read_text(encoding="utf-8")
    start = source.index('"method": {"const": "metadata.objects.list"}')
    end = source.index('"method": {"const": "metadata.object.full"}', start)
    block = source[start:end]

    assert '"extension"' not in block
    assert '"include_extensions"' not in block


def test_mcp_schema_template_selector_properties_stay_complete() -> None:
    assert set(adapter_mcp.OBJECT_SELECTOR_SCHEMA_PROPERTIES) == set(adapter_server.OBJECT_SELECTOR_ARGUMENTS)

    source = Path(adapter_mcp.__file__).read_text(encoding="utf-8")
    assert '"required": ["base_id", "object_type", "object_name"]' not in source


def test_mcp_adapter_contract_script_checks_selector_guidance() -> None:
    report = mcp_adapter_contract.check_contract()

    assert report["passed"] is True
    assert report["checks"]["generic_onec_request_schema"] is True
    assert report["checks"]["adapter_help_selector_guidance"] is True
    assert report["checks"]["adapter_selector_argument_type_validation"] is True
    assert report["checks"]["adapter_selector_normalizer_type_validation"] is True
    assert report["checks"]["adapter_selector_required_message"] is True
    assert report["checks"]["adapter_selector_presence_aliases"] is True
    assert report["checks"]["adapter_selector_capability_descriptor"] is True
    assert report["checks"]["adapter_definition_read_selector_public_ref"] is True
    assert report["checks"]["adapter_module_read_selector_public_ref"] is True
    assert report["checks"]["agent_facing_object_methods_name_first"] is True
    assert report["checks"]["mcp_selector_schema_template"] is True
    assert report["checks"]["mcp_tool_selector_guidance"] is True
    assert report["checks"]["selector_chain_working_state"] is True
    assert report["checks"]["no_concrete_selector_values_in_production"] is True
    assert report["production_selector_files"] > 0
    assert not report["issues"]


def test_mcp_adapter_contract_scans_production_selector_files() -> None:
    scanned = {path.relative_to(ROOT).as_posix() for path in mcp_adapter_contract.iter_production_selector_contract_files()}

    assert "plugins/1c/connector/adapter_1c_server.py" in scanned
    assert "plugins/1c/mcp/adapter_1c_mcp.py" in scanned
    assert "docs/1c-adapter-api-contract.md" in scanned
    assert not any("__pycache__" in path for path in scanned)


def test_mcp_adapter_contract_maps_selector_schema_issues_to_check() -> None:
    checks = mcp_adapter_contract.contract_checks(
        [{"code": "mcp_schema_requires_object_type_name_selector"}],
        onec_request_present=True,
    )

    assert checks["mcp_selector_schema_template"] is False


def test_mcp_adapter_contract_selector_issue_codes_flip_a_check() -> None:
    issue_codes = [
        "mcp_tool_selector_guidance_missing",
        "mcp_examples_public_ref_placeholder_missing",
        "adapter_help_selector_guidance_missing",
        "adapter_selector_argument_type_not_validated",
        "adapter_selector_normalizer_type_not_validated",
        "adapter_selector_required_message_incomplete",
        "adapter_selector_message_constant_incomplete",
        "adapter_selector_presence_alias_mismatch",
        "adapter_selector_capability_descriptor_mismatch",
        "adapter_help_selector_capabilities_missing",
        "adapter_definition_read_selector_public_ref_missing",
        "adapter_module_read_selector_public_ref_missing",
        "agent_facing_object_method_not_name_first",
        "adapter_contract_version_missing",
        "mcp_contract_version_mismatch",
        "adapter_help_contract_version_missing",
        "mcp_selector_schema_properties_mismatch",
        "mcp_schema_requires_object_type_name_selector",
        "selector_chain_working_state_missing",
        "production_concrete_selector_value_present",
    ]

    for code in issue_codes:
        checks = mcp_adapter_contract.contract_checks([{"code": code}], onec_request_present=True)
        assert any(value is False for value in checks.values()), code


def test_mcp_selector_chain_smoke_uses_generic_placeholders() -> None:
    report = mcp_selector_chain_smoke.build_report()

    assert report["passed"] is True
    assert report["schema"] == "onec_mcp_selector_chain_smoke.v1"
    assert report["counts"]["chains"] >= 3
    assert report["counts"]["steps"] >= 6
    examples_json = json.dumps(report["chains"], ensure_ascii=False)
    assert "<base_id-from-project-context>" in examples_json
    assert "<metadata-kind>.<metadata-object-name>" in examples_json
    assert "<item.read_selector>" in examples_json
    code_search_steps = [
        step
        for chain in report["chains"]
        for step in chain.get("steps") or []
        if isinstance(step, dict) and step.get("method") == "code.search"
    ]
    assert code_search_steps
    assert all((step.get("payload") or {}).get("source_state") == "working" for step in code_search_steps)
    form_chain = next(chain for chain in report["chains"] if chain["name"] == "report_to_separate_form_description")
    form_steps = form_chain["steps"]
    assert [step["method"] for step in form_steps] == [
        "metadata.object.forms",
        "metadata.object.form.details",
        "metadata.form.decode",
    ]
    assert all(step["payload"]["source_state"] == "working" for step in form_steps)
    assert form_steps[1]["payload"]["form"] == "<form-name-returned-by-metadata.object.forms>"
    assert form_steps[2]["payload"]["view"] == "structure"
    for concrete_value in mcp_selector_chain_smoke.FORBIDDEN_CONCRETE_SELECTOR_VALUES:
        assert concrete_value not in examples_json


def test_mcp_selector_chain_selects_name_first_form_write_plan_candidate() -> None:
    candidate = mcp_selector_chain_smoke.first_public_form_write_plan_candidate(
        {
            "forms": [
                {
                    "selector": {
                        "extension": "test2",
                        "ref": "Catalog.test2",
                        "form": "t_Форма",
                    },
                    "matches": [
                        {
                            "selector": {
                                "extension": "test2",
                                "ref": "Catalog.test2",
                                "form": "t_Форма",
                                "command": "ЗаменаДомена",
                            },
                            "writable_properties": [
                                {
                                    "property": "name",
                                    "presentation": "Имя",
                                    "value": "ЗаменаДомена",
                                    "value_type": "string",
                                },
                                {
                                    "property": "title",
                                    "presentation": "Заголовок",
                                    "value": "Замена домена",
                                    "value_type": "string",
                                },
                            ],
                        }
                    ],
                }
            ]
        }
    )

    assert candidate == {
        "target": {
            "kind": "form",
            "extension": "test2",
            "ref": "Catalog.test2",
            "form": "t_Форма",
            "command": "ЗаменаДомена",
        },
        "edit": {
            "property": "Заголовок",
            "value": "Замена домена [NAME-FIRST PLAN]",
        },
    }
    assert not any(key in candidate["target"] for key in ("table", "file_name", "guid", "form_guid"))


def test_mcp_selector_chain_form_composition_strict_check() -> None:
    missing = mcp_selector_chain_smoke.live_form_write_plan_composition_required_issue(
        {
            "form_write_plan_composition": {
                "attempted": True,
                "status": "skipped_no_public_form_target",
                "composed": False,
                "name_first": False,
            }
        }
    )
    ready = mcp_selector_chain_smoke.live_form_write_plan_composition_required_issue(
        {
            "form_write_plan_composition": {
                "attempted": True,
                "status": "planned",
                "composed": True,
                "name_first": True,
            }
        }
    )

    assert missing["code"] == "live_form_write_plan_composition_required"
    assert ready is None


def test_mcp_selector_chain_smoke_rejects_concrete_selector_values() -> None:
    issues = mcp_selector_chain_smoke.validate_chain(
        {
            "name": "bad_chain",
            "steps": [
                {
                    "tool": "onec_request",
                    "method": "metadata.definition.find",
                    "payload": {"base_id": "<base_id-from-project-context>", "query": "ПечатьЭтикеток"},
                }
            ],
        }
    )

    assert any(issue["code"] == "chain_contains_concrete_selector_value" for issue in issues)


def test_mcp_selector_chain_smoke_rejects_working_analysis_without_source_state() -> None:
    issues = mcp_selector_chain_smoke.validate_chain(
        {
            "name": "stale_code_search",
            "steps": [
                {
                    "tool": "onec_request",
                    "method": "code.search",
                    "payload": {"base_id": "base_for_test", "query": "Процедура"},
                }
            ],
        }
    )

    assert any(issue["code"] == "chain_step_working_state_missing" for issue in issues)


def selector_chain_help_methods_response() -> dict[str, Any]:
    methods = []
    for method in mcp_selector_chain_smoke.LIVE_HELP_SELECTOR_METHODS:
        capabilities = {
            "accepts_ref": True,
            "accepts_object_aliases": True,
            "requires_query": method in {"metadata.definition.find", "code.search"},
        }
        if method == "metadata.definition.find":
            capabilities["allows_global_areas"] = True
        if method in {"modules.read", "code.read"}:
            capabilities["accepts_module_ref"] = True
        methods.append({"name": method, "selector_capabilities": capabilities})
    return {"status": "ok", "contract_version": mcp_selector_chain_smoke.EXPECTED_CONTRACT_VERSION, "methods": methods}


def test_mcp_selector_chain_live_report_uses_discovered_ref(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[tuple[str, dict[str, Any]]] = []

    def fake_rpc_call(adapter_url: str, method: str, payload: dict[str, Any], *, timeout: float, transport: str = "rest", session_id: str | None = None) -> dict[str, Any]:
        calls.append((method, payload))
        if method == "help.methods":
            return selector_chain_help_methods_response()
        if method == "metadata.objects.list":
            if payload.get("kind") == "DataProcessor":
                return {
                    "status": "ok",
                    "objects": [
                        {
                            "kind": "DataProcessor",
                            "name": "DiscoveredObject",
                            "guid": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec",
                        }
                    ],
                }
            return {"status": "ok", "objects": []}
        if method == "metadata.object.modules":
            return {"status": "ok", "modules": [{"module_ordinal": 1, "name": "ObjectModule"}]}
        if method == "metadata.definition.find":
            assert payload["query"] == "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec"
            return {
                "status": "ok",
                "related_selectors": {
                    "modules": {
                        "base_id": payload["base_id"],
                        "method": "metadata.object.modules",
                        "kind": "DataProcessor",
                        "name": "DiscoveredObject",
                        "ref": "DataProcessor.DiscoveredObject",
                    }
                },
            }
        if method == "modules.read":
            return {"status": "ok", "text": "Процедура Команда()\nКонецПроцедуры"}
        if method == "code.search":
            return {
                "status": "ok",
                "items": [
                    {
                        "read_selector": {
                            "method": "code.read",
                            "base_id": payload["base_id"],
                            "ref": payload["ref"],
                            "module_ordinal": payload["module_ordinal"],
                        }
                    }
                ],
            }
        if method == "code.read":
            return {"status": "ok", "text": "Процедура Команда()\nКонецПроцедуры"}
        return {"status": "error", "error": method}

    monkeypatch.setattr(mcp_selector_chain_smoke, "rpc_call", fake_rpc_call)

    report = mcp_selector_chain_smoke.build_live_report(
        "http://adapter.example",
        "base_for_test",
        timeout=1,
        kinds=("DataProcessor",),
    )

    assert report["passed"] is True
    assert report["object"]["ref"] == "DataProcessor.DiscoveredObject"
    assert any(method == "modules.read" and payload["ref"] == "DataProcessor.DiscoveredObject" for method, payload in calls)
    assert any(method == "code.search" and payload["query"] == "Команда" and payload["state"] == "working" for method, payload in calls)
    assert any(method == "code.read" and payload["ref"] == "DataProcessor.DiscoveredObject" for method, payload in calls)


def test_mcp_selector_chain_live_report_uses_canonical_definition_object(monkeypatch: pytest.MonkeyPatch) -> None:
    def fake_rpc_call(adapter_url: str, method: str, payload: dict[str, Any], *, timeout: float, transport: str = "rest", session_id: str | None = None) -> dict[str, Any]:
        if method == "help.methods":
            return selector_chain_help_methods_response()
        if method == "metadata.objects.list":
            return {
                "status": "ok",
                "objects": [
                    {
                        "kind": "Document",
                        "name": "RawDisplayName",
                        "guid": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec",
                    }
                ],
            }
        if method == "metadata.object.modules":
            return {"status": "ok", "modules": [{"module_ordinal": 1}]}
        if method == "metadata.object.get":
            return {
                "status": "ok",
                "object": {
                    "kind": "Document",
                    "name": "CanonicalName",
                    "guid": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec",
                },
            }
        if method == "metadata.definition.find":
            return {
                "status": "ok",
                "object": {
                    "kind": "Document",
                    "name": "CanonicalName",
                    "guid": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec",
                },
                "related_selectors": {
                    "modules": {
                        "base_id": payload["base_id"],
                        "method": "metadata.object.modules",
                        "kind": "Document",
                        "name": "CanonicalName",
                        "guid": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec",
                        "ref": "Document.CanonicalName",
                    }
                },
            }
        if method == "modules.read":
            return {"status": "ok", "text": "Процедура Команда()\nКонецПроцедуры"}
        if method == "code.search":
            return {"status": "ok", "items": [{"read_selector": {"method": "code.read", "base_id": payload["base_id"], "ref": payload["ref"]}}]}
        if method == "code.read":
            return {"status": "ok", "text": "Процедура Команда()\nКонецПроцедуры"}
        return {"status": "error", "error": method}

    monkeypatch.setattr(mcp_selector_chain_smoke, "rpc_call", fake_rpc_call)

    report = mcp_selector_chain_smoke.build_live_report("http://adapter.example", "base_for_test", timeout=1, kinds=("Document",))

    assert report["passed"] is True
    assert report["object"]["name"] == "CanonicalName"
    assert report["object"]["ref"] == "Document.CanonicalName"


def test_mcp_selector_chain_live_report_accepts_explicit_object_selector(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[tuple[str, dict[str, Any]]] = []
    selector_guid = "00000000-0000-4000-8000-000000000001"

    def fake_rpc_call(adapter_url: str, method: str, payload: dict[str, Any], *, timeout: float, transport: str = "rest", session_id: str | None = None) -> dict[str, Any]:
        calls.append((method, payload))
        if method == "help.methods":
            return selector_chain_help_methods_response()
        if method == "metadata.object.modules":
            assert payload["guid"] == selector_guid
            return {"status": "ok", "modules": [{"module_ordinal": 1}], "object": {"kind": "Document", "name": "SelectedObject", "guid": payload["guid"]}}
        if method == "metadata.object.get":
            return {"status": "ok", "object": {"kind": "Document", "name": "SelectedObject", "guid": payload["guid"]}}
        if method == "metadata.definition.find":
            return {
                "status": "ok",
                "object": {"kind": "Document", "name": "SelectedObject", "guid": payload["guid"]},
                "related_selectors": {
                    "modules": {
                        "base_id": payload["base_id"],
                        "method": "metadata.object.modules",
                        "kind": "Document",
                        "name": "SelectedObject",
                        "guid": payload["guid"],
                        "ref": "Document.SelectedObject",
                    }
                },
            }
        if method == "modules.read":
            return {"status": "ok", "text": "Процедура Команда()\nКонецПроцедуры"}
        if method == "code.search":
            return {"status": "ok", "items": [{"read_selector": {"method": "code.read", "base_id": payload["base_id"], "ref": payload["ref"]}}]}
        if method == "code.read":
            return {"status": "ok", "text": "Процедура Команда()\nКонецПроцедуры"}
        return {"status": "error", "error": method}

    monkeypatch.setattr(mcp_selector_chain_smoke, "rpc_call", fake_rpc_call)

    report = mcp_selector_chain_smoke.build_live_report(
        "http://adapter.example",
        "base_for_test",
        timeout=1,
        kinds=("Catalog",),
        object_selector={"kind": "Document", "guid": selector_guid},
    )

    assert report["passed"] is True
    assert report["object"]["ref"] == "Document.SelectedObject"
    assert not any(method == "metadata.objects.list" for method, _ in calls)


def test_mcp_selector_chain_live_report_requires_related_ref(monkeypatch: pytest.MonkeyPatch) -> None:
    def fake_rpc_call(adapter_url: str, method: str, payload: dict[str, Any], *, timeout: float, transport: str = "rest", session_id: str | None = None) -> dict[str, Any]:
        if method == "help.methods":
            return selector_chain_help_methods_response()
        if method == "metadata.objects.list":
            return {"status": "ok", "objects": [{"kind": "DataProcessor", "name": "DiscoveredObject"}]}
        if method == "metadata.object.modules":
            return {"status": "ok", "modules": [{"module_ordinal": 1}]}
        if method == "metadata.definition.find":
            assert payload["query"] == "DataProcessor.DiscoveredObject"
            return {
                "status": "ok",
                "related_selectors": {
                    "modules": {
                        "base_id": payload["base_id"],
                        "method": "metadata.object.modules",
                        "kind": "DataProcessor",
                        "name": "DiscoveredObject",
                    }
                },
            }
        if method == "modules.read":
            return {"status": "ok", "text": ""}
        return {"status": "error"}

    monkeypatch.setattr(mcp_selector_chain_smoke, "rpc_call", fake_rpc_call)

    report = mcp_selector_chain_smoke.build_live_report("http://adapter.example", "base_for_test", timeout=1, kinds=("DataProcessor",))

    assert report["passed"] is False
    assert any(issue["code"] == "live_related_selector_ref_missing" for issue in report["issues"])


def test_mcp_selector_chain_mcp_transport_unwraps_tool_text(monkeypatch: pytest.MonkeyPatch) -> None:
    captured: dict[str, Any] = {}

    def fake_post_json(url: str, payload: dict[str, Any], *, timeout: float, headers: dict[str, str] | None = None) -> tuple[dict[str, str], dict[str, Any]]:
        captured.update({"url": url, "payload": payload, "headers": headers})
        return (
            {},
            {
                "jsonrpc": "2.0",
                "id": payload["id"],
                "result": {
                    "content": [
                        {
                            "type": "text",
                            "text": json.dumps({"status": "ok", "objects": []}, ensure_ascii=False),
                        }
                    ]
                },
            },
        )

    monkeypatch.setattr(mcp_selector_chain_smoke, "post_json", fake_post_json)

    result = mcp_selector_chain_smoke.mcp_rpc_call(
        "http://mcp.example",
        "metadata.objects.list",
        {"base_id": "base_for_test", "kind": "DataProcessor"},
        timeout=1,
    )

    assert result == {"status": "ok", "objects": []}
    assert captured["url"] == "http://mcp.example/mcp"
    assert captured["payload"]["method"] == "tools/call"
    assert captured["payload"]["params"]["name"] == "onec_request"
    assert captured["payload"]["params"]["arguments"]["method"] == "metadata.objects.list"
    assert captured["headers"]["Accept"] == "application/json, text/event-stream"


def test_mcp_selector_chain_rest_transport_uses_process_service_token(monkeypatch: pytest.MonkeyPatch) -> None:
    captured: dict[str, Any] = {}

    class FakeResponse:
        headers: dict[str, str] = {}

        def __enter__(self) -> "FakeResponse":
            return self

        def __exit__(self, *args: Any) -> None:
            return None

        def read(self) -> bytes:
            return b'{"status":"ok"}'

    def fake_urlopen(request: Any, *, timeout: float) -> FakeResponse:
        captured["authorization"] = request.get_header("Authorization")
        return FakeResponse()

    monkeypatch.setenv("ONEC_ADAPTER_SERVICE_TOKEN", "transient-test-token")
    monkeypatch.setattr(mcp_selector_chain_smoke, "urlopen", fake_urlopen)

    result = mcp_selector_chain_smoke.rest_rpc_call(
        "http://adapter.example",
        "adapter.help.methods",
        {},
        timeout=1,
    )

    assert result == {"status": "ok"}
    assert captured["authorization"] == "Bearer transient-test-token"


def test_related_form_sections_follow_live_object_descriptor_layouts() -> None:
    assert adapter_server.RELATED_SECTION_RULES["Report"] == [
        {"path": "3", "category": "Template"},
        {"path": "5", "category": "Form"},
        {"path": "7", "category": "Command"},
    ]
    assert adapter_server.RELATED_SECTION_RULES["DataProcessor"] == [
        {"path": "4", "category": "Template"},
        {"path": "5", "category": "Command"},
        {"path": "6", "category": "Form"},
    ]
    assert adapter_server.RELATED_SECTION_RULES["Catalog"] == [
        {"path": "3", "category": "Template"},
        {"path": "4", "category": "Command"},
        {"path": "7", "category": "Form"},
    ]
    assert adapter_server.RELATED_SECTION_RULES["Enum"] == [
        {"path": "3", "category": "Form"},
        {"path": "4", "category": "Template"},
    ]
    assert adapter_server.RELATED_SECTION_RULES["InformationRegister"] == [
        {"path": "5", "category": "Form"},
        {"path": "6", "category": "Template"},
        {"path": "8", "category": "Command"},
    ]
    assert adapter_server.RELATED_SECTION_RULES["AccumulationRegister"] == [
        {"path": "4", "category": "Command"},
        {"path": "8", "category": "Form"},
    ]
    assert adapter_server.RELATED_SECTION_RULES["BusinessProcess"] == [{"path": "4", "category": "Form"}]
    assert adapter_server.RELATED_SECTION_RULES["Task"] == [
        {"path": "4", "category": "Form"},
        {"path": "8", "category": "Command"},
    ]
    assert adapter_server.RELATED_SECTION_RULES["ChartOfCharacteristicTypes"] == [
        {"path": "4", "category": "Template"},
        {"path": "7", "category": "Form"},
    ]
    assert adapter_server.RELATED_SECTION_RULES["ChartOfAccounts"] == [{"path": "6", "category": "Form"}]
    assert adapter_server.RELATED_SECTION_RULES["ChartOfCalculationTypes"] == [{"path": "7", "category": "Form"}]
    assert adapter_server.RELATED_SECTION_RULES["DocumentJournal"] == [
        {"path": "3", "category": "Template"},
        {"path": "5", "category": "Command"},
        {"path": "6", "category": "Form"},
    ]
    assert adapter_server.RELATED_SECTION_RULES["ExchangePlan"] == [
        {"path": "4", "category": "Template"},
        {"path": "6", "category": "Form"},
        {"path": "7", "category": "Command"},
    ]
    assert adapter_server.RELATED_SECTION_RULES["SelectionCriterion"] == [{"path": "3", "category": "Form"}]
    assert adapter_server.RELATED_SECTION_RULES["SettingsStorage"] == [{"path": "4", "category": "Form"}]
    for kind in (
        "Enum",
        "InformationRegister",
        "AccumulationRegister",
        "BusinessProcess",
        "Task",
        "DocumentJournal",
        "SettingsStorage",
    ):
        assert "forms" in adapter_server.KIND_CAPABILITIES[kind]


def test_public_child_identity_falls_back_to_inline_record_identity() -> None:
    result = adapter_server.public_child_identity(
        {
            "guid": "00000000-0000-0000-0000-000000000001",
            "status": "source_missing",
            "record_identity": {
                "guid": "00000000-0000-0000-0000-000000000002",
                "name": "КомандаОбъекта",
                "synonyms": {"ru": "Команда объекта"},
            },
        }
    )

    assert result["guid"] == "00000000-0000-0000-0000-000000000002"
    assert result["name"] == "КомандаОбъекта"
    assert result["synonym"] == "Команда объекта"


def test_mcp_selector_chain_live_mcp_transport_initializes_session(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[tuple[str, str | None]] = []

    monkeypatch.setattr(
        mcp_selector_chain_smoke,
        "mcp_initialize",
        lambda mcp_url, *, timeout: (
            "session-for-test",
            {
                "jsonrpc": "2.0",
                "result": {"serverInfo": {"contract_version": mcp_selector_chain_smoke.EXPECTED_CONTRACT_VERSION}},
            },
        ),
    )
    monkeypatch.setattr(
        mcp_selector_chain_smoke,
        "mcp_onec_request_schema_check",
        lambda mcp_url, *, timeout, session_id: (
            {
                "status": "ok",
                "contract_version": mcp_selector_chain_smoke.EXPECTED_CONTRACT_VERSION,
                "tools": 5,
                "onec_request": True,
                "selector_guidance": True,
                "placeholder_examples": True,
            },
            [],
        ),
    )

    def fake_rpc_call(endpoint_url: str, method: str, payload: dict[str, Any], *, timeout: float, transport: str = "rest", session_id: str | None = None) -> dict[str, Any]:
        calls.append((method, session_id))
        if method == "help.methods":
            return selector_chain_help_methods_response()
        if method == "metadata.objects.list":
            return {"status": "ok", "objects": [{"kind": "DataProcessor", "name": "DiscoveredObject"}]}
        if method == "metadata.object.modules":
            return {"status": "ok", "modules": [{"module_ordinal": 1}]}
        if method == "metadata.definition.find":
            assert payload["query"] == "DataProcessor.DiscoveredObject"
            return {
                "status": "ok",
                "related_selectors": {
                    "modules": {
                        "base_id": payload["base_id"],
                        "method": "metadata.object.modules",
                        "kind": "DataProcessor",
                        "name": "DiscoveredObject",
                        "ref": "DataProcessor.DiscoveredObject",
                    }
                },
            }
        if method == "modules.read":
            return {"status": "ok", "text": "Процедура Команда()\nКонецПроцедуры"}
        if method == "code.search":
            return {
                "status": "ok",
                "items": [
                    {
                        "read_selector": {
                            "method": "code.read",
                            "base_id": payload["base_id"],
                            "ref": payload["ref"],
                            "module_ordinal": payload["module_ordinal"],
                        }
                    }
                ],
            }
        if method == "code.read":
            return {"status": "ok", "text": "Процедура Команда()\nКонецПроцедуры"}
        return {"status": "error", "error": method}

    monkeypatch.setattr(mcp_selector_chain_smoke, "rpc_call", fake_rpc_call)

    report = mcp_selector_chain_smoke.build_live_report(
        "http://mcp.example",
        "base_for_test",
        timeout=1,
        kinds=("DataProcessor",),
        transport="mcp",
    )

    assert report["passed"] is True
    assert report["transport"] == "mcp"
    assert report["steps"][0]["name"] == "mcp.initialize"
    assert report["steps"][0]["status"] == "ok"
    assert report["steps"][0]["session"] is True
    assert report["steps"][0]["contract_version"] == mcp_selector_chain_smoke.EXPECTED_CONTRACT_VERSION
    assert report["steps"][1]["name"] == "mcp.tools.list"
    assert report["steps"][1]["selector_guidance"] is True
    assert report["steps"][2]["name"] == "adapter.help.methods"
    assert report["steps"][2]["selector_capabilities"] is True
    assert calls
    assert all(session_id == "session-for-test" for _, session_id in calls)


def test_mcp_selector_chain_live_report_rejects_stale_adapter_help(monkeypatch: pytest.MonkeyPatch) -> None:
    def fake_rpc_call(adapter_url: str, method: str, payload: dict[str, Any], *, timeout: float, transport: str = "rest", session_id: str | None = None) -> dict[str, Any]:
        if method == "help.methods":
            return {"status": "ok", "contract_version": mcp_selector_chain_smoke.EXPECTED_CONTRACT_VERSION, "methods": [{"name": "code.search"}]}
        return {"status": "error", "error": method}

    monkeypatch.setattr(mcp_selector_chain_smoke, "rpc_call", fake_rpc_call)

    report = mcp_selector_chain_smoke.build_live_report(
        "http://adapter.example",
        "base_for_test",
        timeout=1,
        kinds=("DataProcessor",),
    )

    assert report["passed"] is False
    assert report["steps"][0]["name"] == "adapter.help.methods"
    assert report["steps"][0]["status"] == "error"
    assert any(issue["code"] == "live_adapter_selector_capabilities_missing" for issue in report["issues"])


def test_mcp_selector_chain_live_mcp_transport_rejects_stale_schema(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        mcp_selector_chain_smoke,
        "mcp_initialize",
        lambda mcp_url, *, timeout: (
            "session-for-test",
            {
                "jsonrpc": "2.0",
                "result": {"serverInfo": {"contract_version": mcp_selector_chain_smoke.EXPECTED_CONTRACT_VERSION}},
            },
        ),
    )
    monkeypatch.setattr(
        mcp_selector_chain_smoke,
        "mcp_onec_request_schema_check",
        lambda mcp_url, *, timeout, session_id: (
            {
                "status": "ok",
                "contract_version": mcp_selector_chain_smoke.EXPECTED_CONTRACT_VERSION,
                "tools": 5,
                "onec_request": True,
                "selector_guidance": False,
                "placeholder_examples": False,
            },
            [{"code": "live_mcp_selector_guidance_missing", "term": "ref"}],
        ),
    )

    report = mcp_selector_chain_smoke.build_live_report(
        "http://mcp.example",
        "base_for_test",
        timeout=1,
        kinds=("DataProcessor",),
        transport="mcp",
    )

    assert report["passed"] is False
    assert report["steps"][1]["name"] == "mcp.tools.list"
    assert report["steps"][1]["status"] == "error"
    assert any(issue["code"] == "live_mcp_selector_guidance_missing" for issue in report["issues"])


def test_adapter_selector_diagnostics_stay_generic() -> None:
    source = Path(adapter_server.__file__).read_text(encoding="utf-8")

    assert "Pass object_type/object_name or object_guid" not in source
    assert "Pass an object selector: kind/name or guid" not in source
    assert "guid or kind/name/ordinal" not in source


def test_object_selector_methods_reject_non_string_selector_arguments() -> None:
    for method in adapter_server.OBJECT_SELECTOR_ALIAS_METHODS:
        for argument in adapter_server.OBJECT_SELECTOR_ARGUMENTS:
            result = adapter_server.validate_adapter_job_payload(method, {"base_id": "upo_test", argument: 123})

            assert result is not None, (method, argument)
            assert result["status"] == "invalid_argument", (method, argument)
            assert result["argument"] == argument, (method, argument)


def test_direct_object_selector_validator_rejects_non_string_public_ref() -> None:
    result = adapter_server.validate_metadata_object_forms_payload({"base_id": "upo_test", "ref": 123})

    assert result is not None
    assert result["status"] == "invalid_argument"
    assert result["argument"] == "ref"


def test_direct_job_object_selector_validators_reject_non_string_kind() -> None:
    full_result = adapter_server.validate_metadata_object_full_payload({"base_id": "upo_test", "kind": 123})
    _, attributes_result = adapter_server.validate_metadata_object_attributes_payload({"base_id": "upo_test", "kind": 123})

    assert full_result is not None
    assert full_result["status"] == "invalid_argument"
    assert full_result["argument"] == "kind"
    assert attributes_result is not None
    assert attributes_result["status"] == "invalid_argument"
    assert attributes_result["argument"] == "kind"


def test_direct_object_scoped_runtime_methods_reject_non_string_kind() -> None:
    cases = [
        adapter_server.code_search({"base_id": "upo_test", "query": "Команда", "kind": 123}),
        adapter_server.read_module({"base_id": "upo_test", "module_ordinal": 1, "kind": 123}),
        adapter_server.metadata_object_forms({"base_id": "upo_test", "kind": 123}),
    ]

    for result in cases:
        assert result["status"] == "invalid_argument"
        assert result["argument"] == "kind"


def test_has_object_selector_accepts_public_aliases_without_normalization() -> None:
    assert adapter_server.has_object_selector({"ref": "Обработка.Имя"}) is True
    assert adapter_server.has_object_selector({"object_name": "Имя"}) is True
    assert adapter_server.has_object_selector({"object_guid": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec"}) is True
    assert adapter_server.has_object_selector({"object_type": "Обработка"}) is False
    assert adapter_server.has_object_selector({"kind": "DataProcessor"}) is False


def test_object_selector_required_message_stays_complete() -> None:
    for term in adapter_server.OBJECT_SELECTOR_GUIDANCE_TERMS:
        assert term in adapter_server.OBJECT_SELECTOR_REQUIRED_MESSAGE
    assert "ordinal" in adapter_server.OBJECT_SELECTOR_REQUIRED_MESSAGE


def test_object_selector_diagnostic_constants_stay_complete() -> None:
    expected_terms = {
        "OBJECT_SELECTOR_REQUIRED_MESSAGE_NO_ORDINAL": adapter_server.OBJECT_SELECTOR_GUIDANCE_TERMS,
        "OBJECT_SELECTOR_GLOBAL_REQUIRED_MESSAGE": (*adapter_server.OBJECT_SELECTOR_GUIDANCE_TERMS, "areas metadata/extensions"),
        "OBJECT_SELECTOR_OR_MODULE_REQUIRED_MESSAGE": (*adapter_server.OBJECT_SELECTOR_GUIDANCE_TERMS, "module_ref", "module_id"),
        "MODULE_READ_SELECTOR_OR_MODULE_ID_MESSAGE": (*adapter_server.OBJECT_SELECTOR_GUIDANCE_TERMS, "ordinal", "module_id"),
    }

    for constant_name, terms in expected_terms.items():
        message = getattr(adapter_server, constant_name)
        for term in terms:
            assert term in message, constant_name


def test_object_selector_capability_descriptor_drives_alias_methods() -> None:
    assert set(adapter_server.OBJECT_SELECTOR_ALIAS_METHODS) == set(adapter_server.OBJECT_SELECTOR_METHOD_CAPABILITIES)
    assert adapter_server.object_selector_capabilities("metadata.definition.find")["allows_global_areas"] is True
    assert adapter_server.object_selector_capabilities("modules.read")["accepts_module_ref"] is True
    assert adapter_server.object_selector_capabilities("code.read")["accepts_module_ref"] is True
    assert adapter_server.object_selector_capabilities("metadata.object.full")["requires_object_selector"] is True
    assert adapter_server.object_selector_capabilities("help.methods") is None


def test_adapter_help_exposes_selector_capabilities_for_object_methods() -> None:
    result = adapter_server.call_method("help.methods", {})
    methods = {row["name"]: row for row in result["methods"]}

    for method in adapter_server.OBJECT_SELECTOR_ALIAS_METHODS:
        capabilities = methods[method].get("selector_capabilities")
        assert isinstance(capabilities, dict), method
        assert capabilities["accepts_ref"] is True, method
        assert capabilities["accepts_kind_name_guid"] is True, method
        assert capabilities["accepts_object_aliases"] is True, method


def test_mcp_enriches_code_search_read_selector_display() -> None:
    result = {
        "status": "ok",
        "items": [
            {
                "match": "Фрагмент",
                "read_selector": {
                    "base_id": "upo_test",
                    "method": "code.read",
                    "ref": "DataProcessor.ТестоваяОбработка",
                    "kind": "DataProcessor",
                    "name": "ТестоваяОбработка",
                    "module_ref": "ConfigCAS:module-guid#stream:0",
                },
            }
        ],
        "diagnostics": {
            "owner_resolution": {
                "strategy": "live_metadata_scan",
                "owner_scan_limit_hit": False,
            }
        },
    }

    enriched = adapter_mcp.maybe_enrich_owner_fields("code.search", {"base_id": "upo_test"}, result)

    item = enriched["items"][0]
    assert item["read_selector"]["method"] == "code.read"
    assert item["read_selector"]["ref"] == "DataProcessor.ТестоваяОбработка"
    assert item["read_selector"]["name"] == "ТестоваяОбработка"
    assert item["read_selector"]["module_ref"] == "ConfigCAS:module-guid#stream:0"
    assert item["source_ref_display"] == {
        "kind": "DataProcessor",
        "name": "ТестоваяОбработка",
        "guid": "ConfigCAS:module-guid#stream:0",
        "method": "code.read",
    }
    assert enriched["diagnostics"]["owner_resolution"]["strategy"] == "live_metadata_scan"


def test_adapter_help_describes_public_read_selectors() -> None:
    result = adapter_server.call_method("help.methods", {})
    methods = {row["name"]: row for row in result["methods"]}

    assert "read_selector.method=modules.read" in methods["modules.search"]["description"]
    assert "diagnostics.owner_resolution" in methods["modules.search"]["description"]
    assert "module_ref from modules.search read_selector" in methods["modules.read"]["description"]
    assert "read_selector.method is code.read" in methods["code.search"]["description"]
    assert "module_ref from code.search read_selector" in methods["code.read"]["description"]
    assert "related_selectors" in methods["metadata.definition.find"]["description"]
    assert "object_type/object_name/object_guid" in methods["metadata.definition.find"]["description"]
    assert "object_type/object_name/object_guid" in methods["modules.search"]["description"]
    assert "object_type/object_name/object_guid" in methods["modules.read"]["description"]
    assert "object_type/object_name/object_guid" in methods["code.search"]["description"]
    assert "object_type/object_name/object_guid" in methods["code.read"]["description"]
    assert "public kind counts" in methods["metadata.adapter.audit"]["description"]
    assert "code carrier matrix" in methods["metadata.adapter.audit"]["description"]
    assert "saved-state layer" in methods["metadata.write.capabilities"]["description"]


def test_metadata_write_capabilities_reports_saved_state_rule() -> None:
    result = adapter_server.call_method("metadata.write.capabilities", {"base_id": "upo_test"})

    assert result["status"] == "ok"
    assert result["default_write_layer"] == "save"
    assert result["write_capabilities"]["bsl_modules"]["agent_method"] == "code.write"
    assert result["write_capabilities"]["scheduled_jobs"]["status"] == "partial_saved_state"
    assert result["write_capabilities"]["scheduled_jobs"]["agent_method"] == "metadata.write"
    assert "web_http_services" in result["unsupported_summary"]
    assert [step["area"] for step in result["workflow_priority"][:3]] == [
        "bsl_modules",
        "report_scd",
        "templates",
    ]
    assert result["write_capabilities"]["report_scd"]["agent_method"] == "scd.patch"
    assert [item["key"] for item in result["registered_handlers"]] == [
        "module", "form", "object_property", "object_member", "scheduled_job_schedule"
    ]


def test_adapter_health_reports_saved_state_write_and_activation_boundary(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setattr(
        adapter_server,
        "sql_config_for_base",
        lambda base_id: (
            {"server": "sql.example", "database": base_id, "user": "adapter"},
            None,
        ),
    )

    result = adapter_server.AdapterState().health(base_id="upo_test")

    assert result["status"] == "ok"
    assert "saved-state-write" in result["capabilities"]
    assert "active-saved-compare" in result["capabilities"]
    assert result["features"]["saved_state_write"]["available"] is True
    assert result["features"]["saved_state_write"]["agent_method"] == "code.write"
    assert result["features"]["active_saved_compare"]["module_text_sha1"] is True
    assert result["features"]["configuration_activation"] == {
        "available": True,
        "status": "debug_acceptance_available",
        "methods": [
            "configuration.activation.status",
            "configuration.activation.plan",
            "configuration.activation.request",
            "configuration.activation.request.status",
            "configuration.activation.request.cancel",
            "configuration.activation.audit",
            "configuration.activation.capabilities",
            "configuration.activation.bridge.probe",
            "configuration.activation.execute",
            "configuration.activation.verify",
        ],
        "debug_acceptance_available": True,
        "execution_available": False,
        "reason": "designer_activation_bridge_not_implemented",
        "saved_state_only": True,
        "read_only": True,
        "state_store": "local_sqlite",
    }


def test_adapter_help_describes_selector_aliases_for_all_object_methods() -> None:
    result = adapter_server.call_method("help.methods", {})
    methods = {row["name"]: row for row in result["methods"]}

    for method in adapter_server.OBJECT_SELECTOR_ALIAS_METHODS:
        assert "object_type/object_name/object_guid" in methods[method]["description"], method


def test_adapter_help_completes_partial_selector_guidance() -> None:
    row = adapter_server.public_method_row(
        {
            "name": "code.search",
            "transport": "POST /rpc",
            "description": "Accepts object_type/object_name/object_guid.",
        }
    )

    assert "ref" in row["description"]
    assert "kind/name/guid" in row["description"]
    assert "object_type/object_name/object_guid" in row["description"]


def test_mcp_unified_methods_do_not_shadow_adapter_methods() -> None:
    adapter_methods = {str(row.get("name") or "") for row in adapter_server.METHODS if row.get("name")}

    assert adapter_methods.isdisjoint(adapter_mcp.UNIFIED_METHODS)


def test_mcp_generic_request_passes_any_adapter_method(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[tuple[str, str, object | None]] = []

    def fake_http_json(method: str, path: str, body: object | None = None, *, timeout: float | None = None) -> dict[str, object]:
        calls.append((method, path, body))
        return {"status": "ok", "method": "future.adapter.method"}

    monkeypatch.setattr(adapter_mcp, "http_json", fake_http_json)

    result = adapter_mcp.call_adapter_method("future.adapter.method", {"base_id": "upo_test", "x": 1})

    assert result["status"] == "ok"
    assert calls == [
        (
            "POST",
            "/rpc",
            {"method": "future.adapter.method", "payload": {"base_id": "upo_test", "x": 1}},
        )
    ]


def test_mcp_access_role_users_tool_forwards_to_adapter(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[tuple[str, dict[str, object]]] = []

    def fake_call_adapter_method(method: str, payload: dict[str, object], *, timeout: float | None = None) -> dict[str, object]:
        calls.append((method, payload))
        return {"status": "ok", "users": [{"name": "Ivan.PETROV"}]}

    monkeypatch.setattr(adapter_mcp, "call_adapter_method", fake_call_adapter_method)

    result = adapter_mcp.handle_tool_call("access_role_users", {"base_id": "upo_test", "role": "запись изменение номенклатура поставщиков", "limit": 50})
    decoded = json.loads(result["content"][0]["text"])

    assert decoded["status"] == "ok"
    assert calls == [
        (
            "access.role.users",
            {"base_id": "upo_test", "role": "запись изменение номенклатура поставщиков", "limit": 50},
        )
    ]


def test_mcp_access_object_subjects_tool_forwards_to_adapter(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[tuple[str, dict[str, object]]] = []

    def fake_call_adapter_method(method: str, payload: dict[str, object], *, timeout: float | None = None) -> dict[str, object]:
        calls.append((method, payload))
        return {"status": "ok", "users": [{"user": {"name": "Ivan.PETROV"}}]}

    monkeypatch.setattr(adapter_mcp, "call_adapter_method", fake_call_adapter_method)

    result = adapter_mcp.handle_tool_call(
        "access_object_subjects",
        {"base_id": "upo_test", "ref": "РегистрСведений.УОП_АктуальныеСпецификации", "action": "write", "include_access_key_scope": True},
    )
    decoded = json.loads(result["content"][0]["text"])

    assert decoded["status"] == "ok"
    assert calls == [
        (
            "access.object.subjects",
            {"base_id": "upo_test", "ref": "РегистрСведений.УОП_АктуальныеСпецификации", "action": "write", "include_access_key_scope": True},
        )
    ]


def test_mcp_access_object_roles_tool_forwards_to_adapter(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[tuple[str, dict[str, object]]] = []

    def fake_call_adapter_method(method: str, payload: dict[str, object], *, timeout: float | None = None) -> dict[str, object]:
        calls.append((method, payload))
        return {"status": "ok", "roles": [{"name": "Базовые права БСП"}]}

    monkeypatch.setattr(adapter_mcp, "call_adapter_method", fake_call_adapter_method)

    result = adapter_mcp.handle_tool_call(
        "access_object_roles",
        {
            "base_id": "upo_test",
            "ref": "РегистрСведений.УОП_АктуальныеСпецификации",
            "action": "write",
            "max_effective_permissions_per_user": 1000,
        },
    )
    decoded = json.loads(result["content"][0]["text"])

    assert decoded["status"] == "ok"
    assert calls == [
        (
            "access.object.roles",
            {
                "base_id": "upo_test",
                "ref": "РегистрСведений.УОП_АктуальныеСпецификации",
                "action": "write",
                "max_effective_permissions_per_user": 1000,
            },
        )
    ]


def test_mcp_access_key_tools_forward_to_adapter(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[tuple[str, dict[str, object]]] = []

    def fake_call_adapter_method(method: str, payload: dict[str, object], *, timeout: float | None = None) -> dict[str, object]:
        calls.append((method, payload))
        return {"status": "ok", "counts": {"rows": 1}}

    monkeypatch.setattr(adapter_mcp, "call_adapter_method", fake_call_adapter_method)

    keys_result = adapter_mcp.handle_tool_call(
        "access_keys_query",
        {"base_id": "upo_test", "kind": "group", "group": "Администраторы", "limit": 5},
    )
    object_keys_result = adapter_mcp.handle_tool_call(
        "access_object_keys_resolve",
        {"base_id": "upo_test", "object": "Catalog.Номенклатура", "limit": 5},
    )

    assert json.loads(keys_result["content"][0]["text"])["status"] == "ok"
    assert json.loads(object_keys_result["content"][0]["text"])["status"] == "ok"
    assert calls == [
        ("access.keys.query", {"base_id": "upo_test", "kind": "group", "group": "Администраторы", "limit": 5}),
        ("access.object_keys.resolve", {"base_id": "upo_test", "object": "Catalog.Номенклатура", "limit": 5}),
    ]


def test_mcp_access_users_search_tool_forwards_to_adapter(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[tuple[str, dict[str, object]]] = []

    def fake_call_adapter_method(method: str, payload: dict[str, object], *, timeout: float | None = None) -> dict[str, object]:
        calls.append((method, payload))
        return {"status": "ok", "users": [{"name": "Lyubov.IVANOVA"}]}

    monkeypatch.setattr(adapter_mcp, "call_adapter_method", fake_call_adapter_method)

    result = adapter_mcp.handle_tool_call("access_users_search", {"base_id": "upo_test", "query": "Lyubov", "limit": 5})
    decoded = json.loads(result["content"][0]["text"])

    assert decoded["status"] == "ok"
    assert calls == [("access.users.search", {"base_id": "upo_test", "query": "Lyubov", "limit": 5})]


def test_mcp_infobase_user_tools_forward_to_default_user_methods(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[tuple[str, dict[str, object]]] = []

    def fake_call_adapter_method(method: str, payload: dict[str, object], *, timeout: float | None = None) -> dict[str, object]:
        calls.append((method, payload))
        return {"status": "ok"}

    monkeypatch.setattr(adapter_mcp, "call_adapter_method", fake_call_adapter_method)

    search = adapter_mcp.handle_tool_call("infobase_users_search", {"base_id": "upo_test", "query": "Аналитик"})
    get_one = adapter_mcp.handle_tool_call("infobase_user_get", {"base_id": "upo_test", "user": "Аналитик"})
    capabilities = adapter_mcp.handle_tool_call("infobase_user_password_capabilities", {"base_id": "upo_test"})
    password_status = adapter_mcp.handle_tool_call("infobase_user_password_status", {"base_id": "upo_test", "user": "Аналитик"})
    password_set = adapter_mcp.handle_tool_call(
        "infobase_user_password_set",
        {
            "base_id": "upo_test",
            "user": "Аналитик",
            "confirm_user_id": "a74272fec8cf262a405a568d7871fb07",
            "new_password": "test-password",
            "allow_password_change": True,
        },
    )
    password_clear = adapter_mcp.handle_tool_call(
        "infobase_user_password_clear",
        {
            "base_id": "upo_test",
            "user": "Аналитик",
            "confirm_user_id": "a74272fec8cf262a405a568d7871fb07",
            "allow_password_clear": True,
        },
    )

    assert json.loads(search["content"][0]["text"])["status"] == "ok"
    assert json.loads(get_one["content"][0]["text"])["status"] == "ok"
    assert json.loads(capabilities["content"][0]["text"])["status"] == "ok"
    assert json.loads(password_status["content"][0]["text"])["status"] == "ok"
    assert json.loads(password_set["content"][0]["text"])["status"] == "ok"
    assert json.loads(password_clear["content"][0]["text"])["status"] == "ok"
    assert calls == [
        ("infobase.users.search", {"base_id": "upo_test", "query": "Аналитик"}),
        ("infobase.user.get", {"base_id": "upo_test", "user": "Аналитик"}),
        ("infobase.user.password.capabilities", {"base_id": "upo_test"}),
        ("infobase.user.password.status", {"base_id": "upo_test", "user": "Аналитик"}),
        (
            "infobase.user.password.set",
            {
                "base_id": "upo_test",
                "user": "Аналитик",
                "confirm_user_id": "a74272fec8cf262a405a568d7871fb07",
                "new_password": "test-password",
                "allow_password_change": True,
            },
        ),
        (
            "infobase.user.password.clear",
            {
                "base_id": "upo_test",
                "user": "Аналитик",
                "confirm_user_id": "a74272fec8cf262a405a568d7871fb07",
                "allow_password_clear": True,
            },
        ),
    ]


def test_mcp_access_rls_discover_tool_forwards_to_adapter(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[tuple[str, dict[str, object]]] = []

    def fake_call_adapter_method(method: str, payload: dict[str, object], *, timeout: float | None = None) -> dict[str, object]:
        calls.append((method, payload))
        return {"status": "ok", "candidates": [{"name": "КлючиДоступаКОбъектам"}]}

    monkeypatch.setattr(adapter_mcp, "call_adapter_method", fake_call_adapter_method)

    result = adapter_mcp.handle_tool_call("access_rls_discover", {"base_id": "upo_test", "terms": ["Доступ"], "limit": 5})
    decoded = json.loads(result["content"][0]["text"])

    assert decoded["status"] == "ok"
    assert calls == [
        (
            "access.rls.discover",
            {"base_id": "upo_test", "terms": ["Доступ"], "limit": 5},
        )
    ]


def test_mcp_access_role_audit_export_tool_forwards_to_adapter(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[tuple[str, dict[str, object]]] = []

    def fake_call_adapter_method(method: str, payload: dict[str, object], *, timeout: float | None = None) -> dict[str, object]:
        calls.append((method, payload))
        return {"status": "ok", "rows": [{"user_name": "Ivan.PETROV"}]}

    monkeypatch.setattr(adapter_mcp, "call_adapter_method", fake_call_adapter_method)

    result = adapter_mcp.handle_tool_call("access_role_audit_export", {"base_id": "upo_test", "role": "запись изменение номенклатура поставщиков", "format": "csv"})
    decoded = json.loads(result["content"][0]["text"])

    assert decoded["status"] == "ok"
    assert calls == [
        (
            "access.role.audit_export",
            {"base_id": "upo_test", "role": "запись изменение номенклатура поставщиков", "format": "csv"},
        )
    ]


def test_mcp_access_role_audit_analyze_tool_forwards_to_adapter(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[tuple[str, dict[str, object]]] = []

    def fake_call_adapter_method(method: str, payload: dict[str, object], *, timeout: float | None = None) -> dict[str, object]:
        calls.append((method, payload))
        return {"status": "ok", "risk_level": "low", "findings": []}

    monkeypatch.setattr(adapter_mcp, "call_adapter_method", fake_call_adapter_method)

    result = adapter_mcp.handle_tool_call("access_role_audit_analyze", {"base_id": "upo_test", "role": "Администратор", "user_threshold": 10})
    decoded = json.loads(result["content"][0]["text"])

    assert decoded["status"] == "ok"
    assert calls == [
        (
            "access.role.audit_analyze",
            {"base_id": "upo_test", "role": "Администратор", "user_threshold": 10},
        )
    ]


def test_mcp_access_role_audit_compare_latest_uses_local_reports(tmp_path: Path) -> None:
    folder = tmp_path / "upo_test"
    folder.mkdir()
    old_export = folder / "old.json"
    new_export = folder / "new.json"
    old_export.write_text(json.dumps({"rows": [{"user_id": "u1", "user_name": "Ivan.PETROV", "access_path": "A"}]}, ensure_ascii=False), encoding="utf-8")
    new_export.write_text(
        json.dumps(
            {
                "rows": [
                    {"user_id": "u1", "user_name": "Ivan.PETROV", "access_path": "A"},
                    {"user_id": "u2", "user_name": "Olga.IVANOVA", "access_path": "B"},
                ]
            },
            ensure_ascii=False,
        ),
        encoding="utf-8",
    )
    for name, generated_at, export_path in [
        ("old.summary.json", "2026-07-01T10:00:00+00:00", old_export),
        ("new.summary.json", "2026-07-02T10:00:00+00:00", new_export),
    ]:
        (folder / name).write_text(
            json.dumps(
                {
                    "schema": "onec_access_role_audit_artifact.v1",
                    "generated_at": generated_at,
                    "query": {"role": "Роль A"},
                    "artifacts": {"json": str(export_path)},
                },
                ensure_ascii=False,
            ),
            encoding="utf-8",
        )

    result = adapter_mcp.handle_tool_call(
        "access_role_audit_compare_latest",
        {"base_id": "upo_test", "role": "Роль A", "report_root": str(tmp_path), "write_artifacts": False},
    )
    decoded = json.loads(result["content"][0]["text"])

    assert decoded["status"] == "ok"
    assert decoded["counts"]["added_users"] == 1
    assert decoded["added_users"][0]["user_name"] == "Olga.IVANOVA"
    assert decoded["sources"]["old"].endswith("old.summary.json")
    assert decoded["sources"]["new"].endswith("new.summary.json")


def test_mcp_access_tools_require_base_id() -> None:
    result = adapter_mcp.handle_tool_call("access_role_users", {"role": "Администратор"})
    decoded = json.loads(result["content"][0]["text"])

    assert decoded["schema"] == "adapter_1c_mcp_policy.v1"
    assert decoded["status"] == "blocked"
    assert decoded["reason"] == "base_id_required"
    assert adapter_mcp.method_requires_base_id("access.role.users") is True


def test_export_1c_access_role_audit_writes_artifacts(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[tuple[str, dict[str, Any]]] = []

    def fake_rpc(base_url: str, method: str, payload: dict[str, Any], *, timeout: int) -> dict[str, Any]:
        calls.append((method, payload))
        if method == "access.role.audit_export":
            return {
                "status": "ok",
                "csv": "base_id,user_name\nupo_test,Ivan.PETROV\n",
                "counts": {"rows": 1, "users": 1},
                "rows": [{"user_name": "Ivan.PETROV", "group_name": "Закупщик", "profile_name": "УПО. Закупки", "matched_role_name": "Роль", "access_path": "Роль -> УПО. Закупки -> Закупщик -> Ivan.PETROV"}],
            }
        if method == "access.role.audit_analyze":
            return {
                "status": "ok",
                "risk_level": "low",
                "findings": [],
            }
        raise AssertionError(method)

    monkeypatch.setattr(access_role_audit_export_script, "rpc", fake_rpc)

    summary = access_role_audit_export_script.export_role_audit(
        adapter_url="http://adapter.example",
        base_id="upo_test",
        role="запись изменение номенклатура поставщиков",
        report_root=tmp_path,
        timeout=120,
        user_threshold=30,
        include_analysis=True,
        limit=20000,
        include_html=True,
    )

    artifacts = summary["artifacts"]
    assert summary["status"] == "ok"
    assert summary["risk_level"] == "low"
    assert Path(artifacts["csv"]).read_text(encoding="utf-8-sig").startswith("base_id,user_name")
    assert json.loads(Path(artifacts["json"]).read_text(encoding="utf-8"))["counts"]["rows"] == 1
    assert json.loads(Path(artifacts["analysis"]).read_text(encoding="utf-8"))["risk_level"] == "low"
    assert json.loads(Path(artifacts["summary"]).read_text(encoding="utf-8"))["schema"] == "onec_access_role_audit_artifact.v1"
    html_text = Path(artifacts["html"]).read_text(encoding="utf-8")
    assert "1C Access Role Audit" in html_text
    assert "Ivan.PETROV" in html_text
    index_text = Path(artifacts["index"]).read_text(encoding="utf-8")
    assert "1C Access Audit Index" in index_text
    assert "запись изменение номенклатура поставщиков" in index_text
    assert "low" in index_text
    assert Path(artifacts["csv"]).name in index_text
    assert calls[0][0] == "access.role.audit_export"
    assert calls[1][0] == "access.role.audit_analyze"


def test_compare_1c_access_role_audit_detects_user_and_path_changes(tmp_path: Path) -> None:
    old_path = tmp_path / "old.json"
    new_path = tmp_path / "new.json"
    output_path = tmp_path / "compare.json"
    html_path = tmp_path / "compare.html"
    old_path.write_text(
        json.dumps(
            {
                "schema": "onec_access_role_audit_export.v1",
                "rows": [
                    {"user_id": "u1", "user_name": "Ivan.PETROV", "access_path": "Роль -> Профиль -> Группа A -> Ivan.PETROV"},
                    {"user_id": "u2", "user_name": "Anna.SIDOROVA", "access_path": "Роль -> Профиль -> Группа A -> Anna.SIDOROVA"},
                ],
            },
            ensure_ascii=False,
        ),
        encoding="utf-8",
    )
    new_path.write_text(
        json.dumps(
            {
                "schema": "onec_access_role_audit_export.v1",
                "rows": [
                    {"user_id": "u1", "user_name": "Ivan.PETROV", "access_path": "Роль -> Профиль -> Группа B -> Ivan.PETROV"},
                    {"user_id": "u3", "user_name": "Olga.IVANOVA", "access_path": "Роль -> Профиль -> Группа B -> Olga.IVANOVA"},
                ],
            },
            ensure_ascii=False,
        ),
        encoding="utf-8",
    )

    result = access_role_audit_compare_script.compare_files(old_path, new_path, output=output_path, html_output=html_path)

    assert result["status"] == "ok"
    assert result["counts"] == {
        "old_users": 2,
        "new_users": 2,
        "added_users": 1,
        "removed_users": 1,
        "unchanged_users": 1,
        "changed_access_paths": 1,
    }
    assert result["added_users"][0]["user_name"] == "Olga.IVANOVA"
    assert result["removed_users"][0]["user_name"] == "Anna.SIDOROVA"
    assert result["changed_access_paths"][0]["user"]["user_name"] == "Ivan.PETROV"
    assert json.loads(output_path.read_text(encoding="utf-8"))["schema"] == "onec_access_role_audit_compare.v1"
    assert "1C Access Audit Compare" in html_path.read_text(encoding="utf-8")


def test_compare_1c_access_role_audit_finds_latest_summaries_by_role(tmp_path: Path) -> None:
    folder = tmp_path / "upo_test"
    folder.mkdir()
    for name, generated_at, role in [
        ("old.summary.json", "2026-07-01T10:00:00+00:00", "Роль A"),
        ("new.summary.json", "2026-07-02T10:00:00+00:00", "Роль A"),
        ("other.summary.json", "2026-07-03T10:00:00+00:00", "Роль B"),
    ]:
        (folder / name).write_text(
            json.dumps({"schema": "onec_access_role_audit_artifact.v1", "generated_at": generated_at, "query": {"role": role}}, ensure_ascii=False),
            encoding="utf-8",
        )

    result = access_role_audit_compare_script.find_latest_summaries(tmp_path, "upo_test", role="Роль A", count=2)

    assert [path.name for path in result] == ["new.summary.json", "old.summary.json"]


def test_export_1c_access_role_audit_index_links_existing_compare(tmp_path: Path) -> None:
    folder = tmp_path / "upo_test"
    folder.mkdir()
    old_summary = folder / "old.summary.json"
    new_summary = folder / "new.summary.json"
    old_summary.write_text(
        json.dumps({"generated_at": "2026-07-01T10:00:00+00:00", "query": {"role": "Роль A"}, "artifacts": {"summary": str(old_summary)}}, ensure_ascii=False),
        encoding="utf-8",
    )
    new_summary.write_text(
        json.dumps({"generated_at": "2026-07-02T10:00:00+00:00", "query": {"role": "Роль A"}, "artifacts": {"summary": str(new_summary)}}, ensure_ascii=False),
        encoding="utf-8",
    )
    (folder / "compare-old-new.html").write_text("<html></html>", encoding="utf-8")

    index_path = access_role_audit_export_script.update_index(tmp_path, "upo_test")
    index_text = index_path.read_text(encoding="utf-8")

    assert "compare_previous" in index_text
    assert "compare-old-new.html" in index_text


def test_audit_1c_access_role_exports_and_compares_previous(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    folder = tmp_path / "upo_test"
    folder.mkdir()
    old_export = folder / "old.json"
    old_summary = folder / "old.summary.json"
    old_export.write_text(json.dumps({"rows": [{"user_id": "u1", "user_name": "Ivan.PETROV", "access_path": "A"}]}, ensure_ascii=False), encoding="utf-8")
    old_summary.write_text(
        json.dumps(
            {
                "schema": "onec_access_role_audit_artifact.v1",
                "generated_at": "2026-07-01T10:00:00+00:00",
                "query": {"role": "Роль A"},
                "artifacts": {"json": str(old_export)},
            },
            ensure_ascii=False,
        ),
        encoding="utf-8",
    )

    def fake_export_role_audit(**kwargs: Any) -> dict[str, Any]:
        new_export = folder / "new.json"
        new_summary = folder / "new.summary.json"
        new_export.write_text(
            json.dumps(
                {
                    "rows": [
                        {"user_id": "u1", "user_name": "Ivan.PETROV", "access_path": "A"},
                        {"user_id": "u2", "user_name": "Olga.IVANOVA", "access_path": "B"},
                    ]
                },
                ensure_ascii=False,
            ),
            encoding="utf-8",
        )
        new_summary.write_text(
            json.dumps(
                {
                    "schema": "onec_access_role_audit_artifact.v1",
                    "status": "ok",
                    "generated_at": "2026-07-02T10:00:00+00:00",
                    "query": {"role": kwargs["role"]},
                    "risk_level": "low",
                    "artifacts": {"json": str(new_export), "summary": str(new_summary)},
                },
                ensure_ascii=False,
            ),
            encoding="utf-8",
        )
        return {"status": "ok", "risk_level": "low", "artifacts": {"summary": str(new_summary)}}

    monkeypatch.setattr(access_role_audit_script.export_script, "export_role_audit", fake_export_role_audit)

    result = access_role_audit_script.audit_role(
        adapter_url="http://adapter.example",
        base_id="upo_test",
        role="Роль A",
        report_root=tmp_path,
        timeout=120,
        user_threshold=30,
        limit=20000,
    )

    assert result["status"] == "ok"
    assert result["verdict"] == "changed"
    assert result["compare"]["counts"]["added_users"] == 1


def test_mcp_bulk_execute_routes_subrequests_to_adapter(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[tuple[str, dict[str, object]]] = []

    monkeypatch.setattr(adapter_mcp, "read_cached_response", lambda cache_key: None)
    monkeypatch.setattr(adapter_mcp, "write_cache_response", lambda *args, **kwargs: None)

    def fake_call_adapter_method(method: str, payload: dict[str, object], *, timeout: float | None = None) -> dict[str, object]:
        calls.append((method, payload))
        return {"status": "ok", "items": [{"method": method}]}

    monkeypatch.setattr(adapter_mcp, "call_adapter_method", fake_call_adapter_method)

    result = adapter_mcp._run_bulk_execute(
        {
            "base_id": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec",
            "requests": [{"method": "code.read", "payload": {"module_ref": "ConfigCAS:module#stream:1"}}],
        },
        0.0,
        "test-request",
    )

    assert result["status"] == "ok"
    assert calls == [
        (
            "code.read",
            {"module_ref": "ConfigCAS:module#stream:1", "base_id": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec"},
        )
    ]


def test_modules_search_validates_query_and_include_storage_types() -> None:
    bad_query = search_modules({"base_id": "upo_test", "query": ["Запрос"]})
    empty_query = search_modules({"base_id": "upo_test", "query": ""})
    bad_include_storage = search_modules({"base_id": "upo_test", "query": "Запрос", "include_storage": "false"})
    bad_limit = search_modules({"base_id": "upo_test", "query": "Запрос", "limit": "1"})
    bad_max_matches = search_modules({"base_id": "upo_test", "query": "Запрос", "max_matches": "3"})
    conflicting_limit = search_modules({"base_id": "upo_test", "query": "Запрос", "limit": 5, "max_matches": 3})
    bad_scan_limit = search_modules({"base_id": "upo_test", "query": "Запрос", "scan_limit": "10"})

    assert bad_query["status"] == "invalid_argument"
    assert bad_query["argument"] == "query"
    assert empty_query["status"] == "invalid_argument"
    assert empty_query["error"] == "invalid_argument"
    assert empty_query["argument"] == "query"
    assert bad_include_storage["status"] == "invalid_argument"
    assert bad_include_storage["argument"] == "include_storage"
    assert bad_limit["status"] == "invalid_argument"
    assert bad_limit["argument"] == "limit"
    assert bad_max_matches["status"] == "invalid_argument"
    assert bad_max_matches["argument"] == "max_matches"
    assert conflicting_limit["status"] == "invalid_argument"
    assert conflicting_limit["argument"] == "max_matches"
    assert bad_scan_limit["status"] == "invalid_argument"
    assert bad_scan_limit["argument"] == "scan_limit"


def test_metadata_objects_list_validates_boolean_flags() -> None:
    for argument in ["include_storage", "include_missing", "only_missing", "exact_counts", "refresh_cache"]:
        result = call_method("metadata.objects.list", {"base_id": "upo_test", "kind": "Document", argument: "true"})
        assert result["status"] == "invalid_argument"
        assert result["argument"] == argument


def test_metadata_objects_list_rejects_extension_filter() -> None:
    result = call_method(
        "metadata.objects.list",
        {"base_id": "upo_test", "kind": "CommonForm", "extension": "test2", "name_filter": "t_Форма"},
    )

    assert result["status"] == "invalid_argument"
    assert result["argument"] == "extension"
    assert result["diagnostics"]["next_method"] == "extension.objects.find"
    assert result["diagnostics"]["next_payload"]["extension"] == "test2"
    assert result["diagnostics"]["next_payload"]["kind"] == "CommonForm"
    assert result["diagnostics"]["next_payload"]["query"] == "t_Форма"


def test_public_metadata_methods_validate_common_argument_types() -> None:
    cases = [
        ("modules.read", {"base_id": "upo_test", "kind": "Document", "name": "X", "module_ordinal": 1, "include_storage": "false"}, "include_storage"),
        ("modules.read", {"base_id": "upo_test", "kind": "Document", "name": "X", "module_ordinal": 1, "include_text": "false"}, "include_text"),
        ("modules.read", {"base_id": "upo_test", "kind": "Document", "name": "X", "module_ordinal": 1, "summary": "true"}, "summary"),
        ("modules.read", {"base_id": "upo_test", "kind": "Document", "name": "X", "module_ordinal": 1, "routines_only": "true"}, "routines_only"),
        ("modules.read", {"base_id": "upo_test", "kind": "Document", "name": "X", "module_ordinal": 1, "include_container_preview": "true"}, "include_container_preview"),
        ("modules.read", {"base_id": "upo_test", "kind": "Document", "name": "X", "module_ordinal": 1, "include_container_preview": True, "container_preview_chars": "10"}, "container_preview_chars"),
        ("metadata.object.form.details", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_storage": "false"}, "include_storage"),
        ("metadata.object.form.details", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_module_text": "true"}, "include_module_text"),
        ("metadata.object.form.details", {"base_id": "upo_test", "kind": "Document", "name": "X", "max_forms": "1"}, "max_forms"),
        ("metadata.object.form.details", {"base_id": "upo_test", "kind": "Document", "name": "X", "max_items": "10"}, "max_items"),
        ("metadata.form.decode", {"base_id": "upo_test", "kind": "Document", "name": "X", "form": "Y", "include_storage": "false"}, "include_storage"),
        ("metadata.form.decode", {"base_id": "upo_test", "kind": "Document", "name": "X", "form": "Y", "include_module_text": "true"}, "include_module_text"),
        ("metadata.form.decode", {"base_id": "upo_test", "kind": "Document", "name": "X", "form": "Y", "max_items": "10"}, "max_items"),
        ("metadata.form.decode", {"base_id": "upo_test", "kind": "Document", "name": "X", "form": "Y", "evidence_mode": "wide"}, "evidence_mode"),
        ("metadata.object.template.details", {"base_id": "upo_test", "kind": "Document", "name": "X", "template": "Y", "include_storage": "false"}, "include_storage"),
        ("metadata.object.template.details", {"base_id": "upo_test", "kind": "Document", "name": "X", "template": "Y", "include_preview": "true"}, "include_preview"),
        ("metadata.object.template.details", {"base_id": "upo_test", "kind": "Document", "name": "X", "template": "Y", "evidence_mode": "wide"}, "evidence_mode"),
        ("metadata.object.commands", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_storage": "false"}, "include_storage"),
        ("metadata.object.commands", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_form_commands": "true"}, "include_form_commands"),
        ("metadata.object.commands", {"base_id": "upo_test", "kind": "Document", "name": "X", "command": ["Записать"]}, "command"),
        ("metadata.object.related", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_storage": "false"}, "include_storage"),
        ("metadata.object.related", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_text": "false"}, "include_text"),
        ("metadata.object.related", {"base_id": "upo_test", "kind": "Document", "name": "X", "guids_per_record": "5"}, "guids_per_record"),
        ("metadata.object.special.details", {"base_id": "upo_test", "kind": "DocumentJournal", "name": "X", "include_column_types": "true"}, "include_column_types"),
        ("metadata.object.properties", {"base_id": "upo_test", "kind": "DocumentJournal", "name": "X", "include_column_types": "true"}, "include_column_types"),
        ("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_storage": "false"}, "include_storage"),
        ("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_module_text": "true"}, "include_module_text"),
        ("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_form_details": "true"}, "include_form_details"),
        ("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_form_module_text": "true"}, "include_form_module_text"),
        ("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_template_details": "true"}, "include_template_details"),
        ("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_template_preview": "true"}, "include_template_preview"),
        ("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_parts_summary": "true"}, "include_parts_summary"),
        ("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "sections": "forms"}, "sections"),
        ("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "sections": {"include": "forms"}}, "sections"),
        ("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "sections": []}, "sections"),
        ("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "sections": ["unknown"]}, "sections"),
        ("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "sections": [1]}, "sections"),
        ("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "max_forms": "1"}, "max_forms"),
        ("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "max_form_items": "10"}, "max_form_items"),
        ("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "evidence_mode": "wide"}, "evidence_mode"),
        ("metadata.object.attributes", {"base_id": "upo_test", "kind": "Document", "name": "X", "refresh_cache": "true"}, "refresh_cache"),
        ("metadata.object.attributes", {"base_id": "upo_test", "kind": "Document", "name": "X", "use_cache": "true"}, "use_cache"),
        ("metadata.object.attributes", {"base_id": "upo_test", "kind": "Document", "name": "X", "limit": "20"}, "limit"),
        ("adapter.job.get", {"job_id": "not_real", "consume": "true"}, "consume"),
        ("adapter.job.get", {"job_id": "not_real", "include_partial_result": "true"}, "include_partial_result"),
        ("adapter.job.get", {"job_id": ["x"]}, "job_id"),
        ("adapter.job.cancel", {"job_id": ["x"]}, "job_id"),
        ("adapter.job.start", {"method": "metadata.object.get", "payload": {"base_id": "upo_test"}, "timeout_seconds": "10"}, "timeout_seconds"),
        ("adapter.job.start", {"method": ["metadata.object.get"], "payload": {"base_id": "upo_test"}}, "method"),
        ("adapter.job.start", {"method": "metadata.object.get", "payload": "bad"}, "payload"),
        ("metadata.snapshot", {"base_id": "upo_test", "include_modules": "true"}, "include_modules"),
        ("metadata.object.decode", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_storage": "false"}, "include_storage"),
        ("metadata.object.decode", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_text": "true"}, "include_text"),
        ("metadata.object.decode", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_tree": "true"}, "include_tree"),
        ("metadata.object.decode", {"base_id": "upo_test", "kind": "Document", "name": "X", "max_depth": "3"}, "max_depth"),
        ("metadata.object.decode", {"base_id": "upo_test", "kind": "Document", "name": "X", "evidence_mode": "wide"}, "evidence_mode"),
        ("metadata.object.parts", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_storage": "false"}, "include_storage"),
        ("metadata.object.parts", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_text": "true"}, "include_text"),
        ("metadata.object.parts", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_tree": "true"}, "include_tree"),
        ("metadata.object.parts", {"base_id": "upo_test", "kind": "Document", "name": "X", "part_limit": "20"}, "part_limit"),
        ("metadata.object.parts", {"base_id": "upo_test", "kind": "Document", "name": "X", "evidence_mode": "wide"}, "evidence_mode"),
        ("schema.tables.list", {"base_id": "upo_test", "limit": "5"}, "limit"),
        ("storage.files.list", {"base_id": "upo_test", "table": "Config", "limit": "5"}, "limit"),
        ("storage.file.get", {"base_id": "upo_test", "table": "Config", "file_name": "x", "include_payload": "true"}, "include_payload"),
        ("codec.decode", {"base_id": "upo_test", "table": "Config", "file_name": "x", "include_text": "true"}, "include_text"),
        ("codec.decode", {"base_id": "upo_test", "table": "Config", "file_name": "x", "include_tree": "true"}, "include_tree"),
        ("payload.diff", {"diagnostic": True, "before": [], "after": {"text": "{1}"}}, "before"),
        ("payload.diff", {"diagnostic": True, "before": {"text": "{1}"}, "after": {"text": "{2}"}, "include_tree_diff": "true"}, "include_tree_diff"),
        ("payload.diff", {"diagnostic": True, "before": {"text": "{1}"}, "after": {"text": "{2}"}, "max_changes": "10"}, "max_changes"),
        ("codec.encode", {"base_id": "upo_test", "table": "Config", "file_name": "x", "text": "", "include_payload": "true"}, "include_payload"),
        ("query.validate", {"base_id": "upo_test", "query": ["select 1"]}, "query"),
        ("query.run", {"base_id": "upo_test", "query": ["select 1"]}, "query"),
        ("query.run", {"base_id": "upo_test", "query": "select 1", "limit": "1"}, "limit"),
        ("metadata.cache.lookup", {"base_id": "upo_test", "kind": ["Document"], "name": "X"}, "kind"),
        ("metadata.cache.lookup", {"base_id": "upo_test", "kind": "Document", "name": ["X"]}, "name"),
        ("metadata.cache.rebuild", {"base_id": "upo_test", "batch_size": "10"}, "batch_size"),
        ("metadata.cache.rebuild", {"base_id": "upo_test", "timeout_seconds": "10"}, "timeout_seconds"),
    ]
    for method, payload, argument in cases:
        result = call_method(method, payload)
        assert result["status"] == "invalid_argument"
        assert result["argument"] == argument


def test_replace_routine_text_append_and_replace() -> None:
    text = "Процедура Один()\nКонецПроцедуры\n"
    appended, append_edit = replace_routine_text(text, "Функция Два()\n\tВозврат 2;\nКонецФункции\n", operation="append")
    replaced, replace_edit = replace_routine_text(appended, "Процедура Один()\n\tСообщить(1);\nКонецПроцедуры\n", operation="replace")

    assert append_edit["status"] == "appended"
    assert replace_edit["status"] == "replaced"
    assert "Функция Два" in replaced
    assert "Сообщить(1)" in replaced


def test_replace_routine_text_preserves_module_eol_style() -> None:
    text = "#Область ПрограммныйИнтерфейс \r\n\r\nПроцедура Один()\r\n\tСообщить(0);\r\nКонецПроцедуры\r\n\r\nФункция Два()\r\n\tВозврат 2;\r\nКонецФункции\r\n"
    replacement = "Процедура Один()\n\tСообщить(1);\nКонецПроцедуры\n"
    replaced, edit = replace_routine_text(text, replacement, operation="replace")

    assert edit["status"] == "replaced"
    assert "#Область ПрограммныйИнтерфейс \r\n" in replaced
    assert "Сообщить(1);\r\nКонецПроцедуры\r\n" in replaced
    assert "Сообщить(1);\nКонецПроцедуры" not in replaced.replace("\r\n", "")


def test_replace_routine_text_preconditions_and_old_sha1() -> None:
    text = "Процедура Один()\n\tСообщить(0);\nКонецПроцедуры\n"
    replacement = "Процедура Один()\n\tСообщить(1);\nКонецПроцедуры\n"
    replaced, edit = replace_routine_text(
        text,
        replacement,
        operation="replace",
        expected_old_contains="Сообщить(0)",
        expected_old_sha1=text_sha1(text.rstrip("\n")),
    )

    assert edit["old_sha1"] == text_sha1(text.rstrip("\n"))
    assert "Сообщить(1)" in replaced

    with pytest.raises(ValueError, match="expected_old_contains"):
        replace_routine_text(text, replacement, operation="replace", expected_old_contains="НетТакогоТекста")

    with pytest.raises(ValueError, match="expected_old_sha1"):
        replace_routine_text(text, replacement, operation="replace", expected_old_sha1="0" * 40)


def test_classify_root5_base64_html_payload() -> None:
    html = b"<!DOCTYPE html><html><body>help</body></html>"
    encoded = base64.b64encode(html).decode("ascii")
    stored = compress_payload(f'{{5,1,"ru",{{#base64:{encoded}}}}}'.encode("utf-8-sig"), "raw_deflate")
    classified = classify_payload(stored)

    assert classified["role"] == "help_or_html_payload"
    assert classified["root"]["root_marker"] == "5"
    assert classified["base64_blocks"][0]["has_html_marker"] is True


def test_decode_form_payload_profile() -> None:
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{10,11111111-1111-1111-1111-111111111111,"ПриОткрытии"},0,0,'
        '{22,{},0,0,0,9,"ФормаКоманднаяПанель",{},{}},0},'
        '"#Область X\nПроцедура ПриОткрытии()\nКонецПроцедуры\nПроцедура Команда1(Команда)\nКонецПроцедуры",'
        '{4,1,{22,{},0,0,0,9,"Объект",{},{} }},'
        '{0,0},'
        '{0,1,{22,{},0,0,0,9,"Команда1",{},{} }},'
        '{0,0},{0,0},0,0}'
    )
    profile = decode_form_payload(parse_brace_text(text))

    assert profile["status"] == "ok"
    assert profile["events"][0]["handler"] == "ПриОткрытии"
    assert profile["items"][0]["name"] == "ФормаКоманднаяПанель"
    assert profile["items"][0]["semantic"]["groups"]["Основные"][0]["name"] == "Идентификатор"
    assert any(item["name"] == "Вид" and item["value"] == "Командная панель" for item in profile["items"][0]["semantic"]["groups"]["Основные"])
    assert profile["items"][0]["semantic"]["coverage"]["status"] in {"ok", "partial"}
    assert "unmapped_parameters" in profile["items"][0]["semantic"]
    assert profile["attributes"][0]["name"] == "Объект"
    assert profile["attributes"][0]["semantic"]["groups"]["Основные"][0]["name"] == "Идентификатор"
    assert profile["commands"][0]["name"] == "Команда1"
    assert profile["module"]["routine_count"] == 2
    assert profile["handler_links"][0]["handler"] == "ПриОткрытии"
    assert profile["handler_links"][0]["status"] == "resolved"
    assert profile["counts"]["resolved_handlers"] == 1
    assert profile["counts"]["missing_handlers"] == 0
    assert profile["command_links"][0]["command"] == "Команда1"
    assert profile["command_links"][0]["status"] == "resolved"
    assert profile["counts"]["resolved_commands"] == 1


def test_decode_form_payload_exposes_input_fields_and_dynamic_list_columns() -> None:
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{0},0,0,'
        '{37,{40,02023637-7868-4a5f-8576-835a76e0c9ba},0,0,0,2,"ОформленоОрганизации",0,0,{1,1,{"ru","Организации"}},{1,0},{1,{13}}},0},'
        '"Процедура ОформленоОрганизацииНачалоВыбора()\nКонецПроцедуры",'
        '{4,2,'
        '{9,{1},0,"Список",{1,1,{"ru","Список"}},{"Pattern"},0,0,0,0,{0,6,"FieldsMapItemId0",{"N",16},"FieldsMapItemName0",{"S","Number"},"FieldsMapItemSecondaryName0",{"S","Номер"}}},'
        '{9,{13},0,"ОрганизацииСАТУРНПредставление",{1,1,{"ru","Организации представление"}},{"Pattern",{"S"}},0,0,0,0,{0,0}}'
        '},'
        '{0,0},'
        '{0,0},'
        '{0,0},{0,0},0,0}'
    )
    profile = decode_form_payload(parse_brace_text(text), max_items=20)
    by_name = {item["name"]: item for item in profile["items"]}

    field_semantic = by_name["ОформленоОрганизации"]["semantic"]["groups"]["Основные"]
    assert by_name["ОформленоОрганизации"]["type_name"] == "Поле ввода"
    assert by_name["ОформленоОрганизации"]["path_to_data"] == "ОрганизацииСАТУРНПредставление"
    assert any(item["name"] == "Вид" and item["value"] == "Поле ввода" for item in field_semantic)
    assert any(item["name"] == "ПутьКДанным" and item["value"] == "ОрганизацииСАТУРНПредставление" for item in field_semantic)

    column = by_name["Номер"]
    assert column["type_name"] == "Колонка динамического списка"
    column_semantic = column["semantic"]["groups"]["Основные"]
    assert any(item["name"] == "ПутьКДанным" and item["value"] == "Список.Number" for item in column_semantic)

    summary_profile = decode_form_payload(parse_brace_text(text), max_items=20, include_parameters=False)
    summary_field = {item["name"]: item for item in summary_profile["items"]}["ОформленоОрганизации"]
    assert "parameters" not in summary_field
    assert summary_field["semantic"]["groups"]["Основные"]
    assert "unmapped_parameters" not in summary_field["semantic"]
    assert "coverage" not in summary_field["semantic"]
    assert all("parameter_index" not in prop for props in summary_field["semantic"]["groups"].values() for prop in props)
    assert all("source" not in prop for props in summary_field["semantic"]["groups"].values() for prop in props)


def test_decode_form_payload_exposes_marker_48_field_data_path() -> None:
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{0},0,0,'
        '{48,{40,02023637-7868-4a5f-8576-835a76e0c9ba},0,0,1,{0,{}},2,"ПолеА",1,0,{1,0},{1,0},{1,{13}}},0},'
        '"",'
        '{4,1,{9,{13},0,"А",{1,1,{"ru","А"}},{"Pattern",{"S"}},0,0,0,0,{0,0}}},'
        '{0,0},'
        '{0,0},'
        '{0,0},{0,0},0,0}'
    )
    profile = decode_form_payload(parse_brace_text(text), max_items=20)
    field = {item["name"]: item for item in profile["items"]}["ПолеА"]

    assert field["marker"] == "48"
    assert field["type_name"] == "Поле ввода"
    assert field["path_to_data"] == "А"
    assert field["data_path_attribute_id"] == "13"
    assert any(item["name"] == "ПутьКДанным" and item["value"] == "А" for item in field["semantic"]["groups"]["Основные"])


def test_form_payload_resolves_composite_tabular_field_data_path() -> None:
    reference = data_path_reference_from_node(parse_brace_text("{2,{7},{1}}"))
    assert reference == {"attribute_id": "7", "field_id": "1"}

    table_attribute = parse_brace_text(
        '{9,{7},0,"ТЗ",{1,1,{"ru","ТЗ"}},{"Pattern",{"#","acf6192e-81ca-46ef-93a6-5a6968b78663"}},'
        '{0,{0,{"B",1},0}},{0,{0,{"B",1},0}},{0,0},3,'
        '{5,1,0,"К1",{1,1,{"ru","К1"}},{"Pattern",{"N",10,0,0}},{0,{0,{"B",1},0}},{0,{0,{"B",1},0}},{0,0},0},'
        '{5,2,0,"К2",{1,1,{"ru","К2"}},{"Pattern",{"N",10,0,0}},{0,{0,{"B",1},0}},{0,{0,{"B",1},0}},{0,0},0}}'
    )
    fields = dynamic_list_fields(table_attribute, owner_name="ТЗ")
    assert [field["path_to_data"] for field in fields] == ["ТЗ.К1", "ТЗ.К2"]

    items = [
        {"marker": "48", "name": "ТЗК1", "data_path_attribute_id": "7", "data_path_field_id": "1", "semantic": {"groups": {}}},
        {"marker": "48", "name": "А", "data_path_attribute_id": "2", "semantic": {"groups": {}}},
    ]
    attributes = [
        {"id": "7", "name": "ТЗ", "dynamic_list_fields": fields},
        {"id": "2", "name": "А", "dynamic_list_fields": [{"id": "2", "path_to_data": "ТЗ.К2"}]},
    ]

    enrich_item_data_paths(items, attributes)

    assert items[0]["path_to_data"] == "ТЗ.К1"
    assert items[1]["path_to_data"] == "А"


def test_tabular_attribute_field_keeps_internal_name_separate_from_title() -> None:
    table_attribute = parse_brace_text(
        '{9,{7},0,"ТЗ",{1,1,{"ru","ТЗ"}},{"Pattern"},0,0,0,0,0,0,0,1,'
        '{5,967,0,"НомерВерсии",{1,1,{"ru","Номер версии"}},{"Pattern"},0,0,0,0}}'
    )

    fields = dynamic_list_fields(table_attribute, owner_name="ТЗ")

    assert fields[0]["name"] == "НомерВерсии"
    assert fields[0]["title"] == "Номер версии"
    assert fields[0]["path_to_data"] == "ТЗ.НомерВерсии"


def test_decode_form_payload_exposes_input_field_usage_and_layout_properties() -> None:
    specific_values = ["0"] * 66
    specific_values[0] = "36"
    specific_values[47] = "1"
    specific_values[12] = "1"
    specific_values[49] = "0"
    specific_values[50] = "28"
    specific_values[13] = "1"
    specific_values[53] = "0"
    specific_values[41] = "0"
    specific_block = "{" + ",".join(specific_values) + "}"
    direct_values = ["0"] * 27
    direct_values[3] = "2"
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{0},0,0,'
        '{37,{40,02023637-7868-4a5f-8576-835a76e0c9ba},0,0,0,2,"ОформленоОрганизации",0,0,{1,1,{"ru","Организации"}},{1,0},{1,{13}},'
        + ",".join(direct_values)
        + ","
        + specific_block
        + '},0},'
        '"",'
        '{4,1,{9,{13},0,"ОрганизацииСАТУРНПредставление",{1,1,{"ru","Организации представление"}},{"Pattern",{"S"}},0,0,0,0,{0,0}}},'
        '{0,0},'
        '{0,0},'
        '{0,0},{0,0},0,0}'
    )
    profile = decode_form_payload(parse_brace_text(text), max_items=20)
    field = {item["name"]: item for item in profile["items"]}["ОформленоОрганизации"]

    assert field["type_name"] == "Поле ввода"
    basic = field["semantic"]["groups"]["Основные"]
    usage = field["semantic"]["groups"]["Использование"]
    layout = field["semantic"]["groups"]["Расположение"]
    assert any(item["name"] == "ПоложениеЗаголовка" and item["value"] == "None" for item in basic)
    assert any(item["name"] == "КнопкаВыпадающегоСписка" and item["value"] is True for item in usage)
    assert any(item["name"] == "КнопкаВыбора" and item["value"] is True for item in usage)
    assert any(item["name"] == "КнопкаОчистки" and item["value"] is True for item in usage)
    assert any(item["name"] == "МаксимальнаяШирина" and item["value"] == "28" for item in layout)
    unmapped_indexes = {item["index"] for item in field["semantic"]["unmapped_parameters"]}
    assert not ({7, 12, 13, 47, 50} & unmapped_indexes)
    assert field["semantic"]["coverage"]["mapped"] >= 9


def test_decode_form_payload_distinguishes_label_field_subtype() -> None:
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{0},0,0,'
        '{37,{40,02023637-7868-4a5f-8576-835a76e0c9ba},0,0,0,1,"Номер",1,0,{1,1,{"ru","Номер"}},{1,0},{1,{3}}},0},'
        '"",'
        '{4,1,{9,{3},0,"Number",{1,1,{"ru","Номер"}},{"Pattern",{"S"}},0,0,0,0,{0,0}}},'
        '{0,0},'
        '{0,0},'
        '{0,0},{0,0},0,0}'
    )
    profile = decode_form_payload(parse_brace_text(text), max_items=20, include_parameters=False)
    field = {item["name"]: item for item in profile["items"]}["Номер"]

    assert field["type_name"] == "Поле надписи"
    assert any(item["name"] == "Вид" and item["value"] == "Поле надписи" for item in field["semantic"]["groups"]["Основные"])
    assert field["path_to_data"] == "Number"
    assert any(item["name"] == "ПутьКДанным" and item["value"] == "Number" for item in field["semantic"]["groups"]["Основные"])


def test_decode_form_payload_exposes_command_button_semantic_properties() -> None:
    values = ["0"] * 52
    values[0] = "31"
    values[5] = '"СписокАрхивироватьДокументы"'
    values[7] = "1"
    values[10] = "3"
    values[11] = "1"
    values[14] = "2"
    values[26] = "1"
    values[31] = "1"
    values[41] = "3"
    values[42] = "3"
    values[43] = "3"
    values[49] = "1"
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{0},0,0,'
        "{"
        + ",".join(values)
        + '},0},'
        '"",'
        '{0,0},'
        '{0,0},'
        '{0,0},'
        '{0,0},{0,0},0,0}'
    )
    profile = decode_form_payload(parse_brace_text(text), max_items=20, include_parameters=False)
    button = {item["name"]: item for item in profile["items"]}["СписокАрхивироватьДокументы"]

    groups = button["semantic"]["groups"]
    assert any(item["name"] == "Вид" and item["value"] == "Кнопка командной панели" for item in groups["Основные"])
    assert any(item["name"] == "Видимость" and item["value"] is True for item in groups["Основные"])
    assert any(item["name"] == "Доступность" and item["value"] is True for item in groups["Основные"])
    assert any(item["name"] == "Отображение" and item["value"] == "Авто" for item in groups["Основные"])
    assert any(item["name"] == "ButtonImportance" and item["value"] == "Main" for item in groups["Основные"])
    assert any(item["name"] == "ПоложениеВКоманднойПанели" and item["value"] == "В дополнительном подменю" for item in groups["Расположение"])
    assert any(item["name"] == "УникальностьКоманды" and item["value"] is True for item in groups["Расположение"])
    assert any(item["name"] == "GroupHorizontalAlign" and item["value"] == "Auto" for item in groups["Расположение"])
    assert any(item["name"] == "GroupVerticalAlign" and item["value"] == "Auto" for item in groups["Расположение"])
    assert all("parameter_index" not in prop for props in groups.values() for prop in props)
    assert all("source" not in prop for props in groups.values() for prop in props)


def test_enrich_command_button_decodes_importance_and_group_alignment() -> None:
    from parser.form_payload import enrich_command_button_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(52)]
    values[0] = atom(31)
    values[11] = atom(0)
    values[41] = atom(2)
    values[42] = atom(2)
    row = {"marker": "31", "semantic": {"groups": {}}}

    enrich_command_button_specific_semantics(row, seq(*values))

    basic = {item["name"]: item["value"] for item in row["semantic"]["groups"]["Основные"]}
    layout = {item["name"]: item["value"] for item in row["semantic"]["groups"]["Расположение"]}
    assert basic["ButtonImportance"] == "Supplementary"
    assert layout["GroupHorizontalAlign"] == "Right"
    assert layout["GroupVerticalAlign"] == "Bottom"


@pytest.mark.parametrize(("secondary_slot", "expected"), [("0", "В дополнительном подменю"), ("1", "InCommandBar")])
def test_enrich_command_button_uses_secondary_location_slot_for_compact_value(
    secondary_slot: str,
    expected: str,
) -> None:
    from parser.form_payload import enrich_command_button_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(52)]
    values[0] = atom(31)
    values[14] = atom(2)
    values[15] = atom(secondary_slot)
    row = {"marker": "31", "semantic": {"groups": {}}}

    enrich_command_button_specific_semantics(row, seq(*values))

    layout = {item["name"]: item["value"] for item in row["semantic"]["groups"]["Расположение"]}
    assert layout["ПоложениеВКоманднойПанели"] == expected


def test_enrich_decoration_decodes_group_alignment() -> None:
    from parser.form_payload import enrich_decoration_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(36)]
    values[0] = atom(12)
    values[32] = atom(2)
    values[33] = atom(1)
    row = {"marker": "12", "semantic": {"groups": {}}}

    enrich_decoration_specific_semantics(row, seq(*values))

    layout = {item["name"]: item["value"] for item in row["semantic"]["groups"]["Расположение"]}
    assert layout["GroupHorizontalAlign"] == "Right"
    assert layout["GroupVerticalAlign"] == "Center"


def test_enrich_decoration_decodes_size_stretch_and_colors() -> None:
    from parser.form_payload import enrich_decoration_specific_semantics

    node = parse_brace_text(
        '{12,{1},0,0,0,0,"Надпись",{1,0},{1,0},1,15,0,1,2,{3,3,{-21}},{7,3,0,1,100},'
        '{0,0,0},1,{5,0,0,3,0,{0,1,0},{3,3,{-35}},{3,4,{0}},{3,0,{0},0,1,0,00000000-0000-0000-0000-000000000000}}}'
    )
    row = {"marker": "12", "semantic": {"groups": {}}}

    enrich_decoration_specific_semantics(row, node)

    props = {item["name"]: item["value"] for values in row["semantic"]["groups"].values() for item in values}
    assert props["Ширина"] == "15"
    assert props["РастягиватьПоГоризонтали"] is True
    assert props["ЦветТекста"] == "style:ButtonTextColor"
    assert props["ЦветФона"] == "style:TableHeaderBackColor"


def test_enrich_decoration_decodes_extended_layout_slots() -> None:
    from parser.form_payload import enrich_decoration_specific_semantics

    values = ["0"] * 36
    values[0] = "12"
    values[6] = '"Шаг"'
    values[11] = "2"
    values[18] = "{5,0,1,3,0,{0},{3,4,{0}},{3,4,{0}},{0}}"
    values[21] = "0"
    values[27] = "0"
    values[28] = "240"
    values[29] = "0"
    values[31] = "3"
    row = {"marker": "12", "semantic": {"groups": {}}}

    enrich_decoration_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))

    props = {item["name"]: item["value"] for items in row["semantic"]["groups"].values() for item in items}
    assert props["Высота"] == "2"
    assert props["Видимость"] is False
    assert props["АвтоМаксимальнаяШирина"] is False
    assert props["МаксимальнаяШирина"] == "240"
    assert props["АвтоМаксимальнаяВысота"] is False
    assert props["МаксимальнаяВысота"] == "3"
    assert props["ГоризонтальноеПоложениеВГруппе"] == "Center"


def test_enrich_radio_button_decodes_tumbler_type() -> None:
    from parser.form_payload import enrich_radio_button_specific_semantics

    values = ["0"] * 40
    values[0] = "37"
    values[5] = "5"
    values[6] = '"Вариант"'
    values[39] = "{8,{0},0,{3,4,{0}},{7,3,0,1,100},{3,4,{0}},0,2,0,0,0,2}"
    row = {"marker": "37", "type_code": "5", "semantic": {"groups": {}}}

    enrich_radio_button_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))

    assert row["semantic"]["groups"]["Прочее"][0]["value"] == "Tumbler"


def test_enrich_input_field_decodes_vertical_group_alignment() -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    values = [atom(0) for _ in range(59)]
    values[0] = atom(37)
    values[53] = atom(3)
    values[54] = atom(0)
    row = {"marker": "37", "semantic": {"groups": {}}}

    enrich_input_field_specific_semantics(row, seq(*values))

    layout = {item["name"]: item["value"] for item in row["semantic"]["groups"]["Расположение"]}
    assert layout["GroupHorizontalAlign"] == "Auto"
    assert layout["GroupVerticalAlign"] == "Top"


def test_enrich_container_decodes_variable_tail_alignment_positions() -> None:
    from parser.form_payload import enrich_container_specific_semantics

    atom = lambda value: {"type": "atom", "value": str(value)}
    seq = lambda *items: {"type": "list", "items": list(items)}
    cases = (
        (32, "Группа", 2, "Bottom"),
        (32, "Командная панель", 1, "Center"),
        (34, "Группа колонок", 1, "Center"),
        (35, "Страницы", 1, "Center"),
        (38, "Группа", 0, "Top"),
    )
    for length, type_name, vertical_code, expected in cases:
        values = [atom(0) for _ in range(length)]
        values[0] = atom(22)
        values[length - 3] = atom(3)
        values[length - 2] = atom(vertical_code)
        row = {"marker": "22", "type_name": type_name, "semantic": {"groups": {}}}

        enrich_container_specific_semantics(row, seq(*values))

        layout = {item["name"]: item["value"] for item in row["semantic"]["groups"]["Расположение"]}
        assert layout["GroupHorizontalAlign"] == "Auto"
        assert layout["GroupVerticalAlign"] == expected


def test_decode_form_payload_exposes_page_group_and_dynamic_list_semantics() -> None:
    page_values = ["0"] * 22
    page_values[0] = "22"
    page_values[5] = "4"
    page_values[6] = '"СтраницаОформлено"'
    page_values[10] = "1"
    group_values = ["0"] * 22
    group_values[0] = "22"
    group_values[5] = "5"
    group_values[6] = '"СтраницаОформленоОтбор"'
    group_values[10] = "1"
    list_values = ["0"] * 22
    list_values[0] = "55"
    list_values[5] = '"Список"'
    list_values[13] = "1"
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{0},0,0,'
        "{"
        + ",".join(page_values)
        + "},{"
        + ",".join(group_values)
        + "},{"
        + ",".join(list_values)
        + '},0},'
        '"",'
        '{0,0},'
        '{0,0},'
        '{0,0},'
        '{0,0},{0,0},0,0}'
    )
    profile = decode_form_payload(parse_brace_text(text), max_items=20, include_parameters=False)
    by_name = {item["name"]: item for item in profile["items"]}

    page = by_name["СтраницаОформлено"]
    page_basic = page["semantic"]["groups"]["Основные"]
    assert page["type_name"] == "Страница"
    assert any(item["name"] == "Вид" and item["value"] == "Страница" for item in page_basic)
    assert any(item["name"] == "Видимость" and item["value"] is True for item in page_basic)

    group = by_name["СтраницаОформленоОтбор"]
    group_basic = group["semantic"]["groups"]["Основные"]
    assert group["type_name"] == "Группа"
    assert any(item["name"] == "Вид" and item["value"] == "Группа" for item in group_basic)
    assert any(item["name"] == "Видимость" and item["value"] is True for item in group_basic)

    dynamic_list = by_name["Список"]
    list_basic = dynamic_list["semantic"]["groups"]["Основные"]
    assert dynamic_list["type_name"] == "Динамический список"
    assert any(item["name"] == "Вид" and item["value"] == "Динамический список" for item in list_basic)
    assert any(item["name"] == "Видимость" and item["value"] is True for item in list_basic)
    assert all("parameter_index" not in prop for row in by_name.values() for props in row["semantic"]["groups"].values() for prop in props)
    assert all("source" not in prop for row in by_name.values() for props in row["semantic"]["groups"].values() for prop in props)


def test_decode_form_payload_handles_shifted_container_type_and_name() -> None:
    group_values = ["0"] * 22
    group_values[0] = "22"
    group_values[5] = '""'
    group_values[6] = "5"
    group_values[7] = '"ГруппаГоризонтальная"'
    group_values[10] = "1"
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{0},0,0,'
        "{"
        + ",".join(group_values)
        + '},0},'
        '"",'
        '{0,0},'
        '{0,0},'
        '{0,0},'
        '{0,0},{0,0},0,0}'
    )
    profile = decode_form_payload(parse_brace_text(text), max_items=20)
    group = profile["items"][0]

    assert group["name"] == "ГруппаГоризонтальная"
    assert group["name_path"] == "1.30.7"
    assert group["type_code"] == "5"
    assert group["type_name"] == "Группа"


def test_decode_form_payload_exposes_extended_tooltip_items() -> None:
    tooltip_values = ["0"] * 20
    tooltip_values[0] = "12"
    tooltip_values[1] = "{40,00000000-0000-0000-0000-000000000068}"
    tooltip_values[2] = "0"
    tooltip_values[3] = "0"
    tooltip_values[4] = "0"
    tooltip_values[5] = "0"
    tooltip_values[6] = '"ПолеРасширеннаяПодсказка"'
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{0},0,0,'
        "{"
        + ",".join(tooltip_values)
        + '},0},'
        '"",'
        '{0,0},'
        '{0,0},'
        '{0,0},'
        '{0,0},{0,0},0,0}'
    )
    profile = decode_form_payload(parse_brace_text(text), max_items=20)
    tooltip = profile["items"][0]

    assert tooltip["name"] == "ПолеРасширеннаяПодсказка"
    assert tooltip["name_path"] == "1.30.6"
    assert tooltip["marker"] == "12"
    assert tooltip["type_code"] == "0"
    assert tooltip["type_name"] == "Расширенная подсказка"
    assert any(item["name"] == "Имя" and item["value"] == "ПолеРасширеннаяПодсказка" for item in tooltip["semantic"]["groups"]["Основные"])


def test_decode_form_payload_reads_form_events_from_common_section() -> None:
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,'
        '{3,3ccc650e-f631-4cae-8e33-3eaac610b5f9,"ПриОткрытии",'
        '9f2e5ddb-3492-4f5d-8f0d-416b8d1d5c5b,"",'
        'e773807c-0c0c-4689-a093-231ddcd6409f,"ПередЗагрузкойДанныхИзНастроекНаСервере",'
        '1,0,3ccc650e-f631-4cae-8e33-3eaac610b5f9,0,1,9f2e5ddb-3492-4f5d-8f0d-416b8d1d5c5b,0,1,e773807c-0c0c-4689-a093-231ddcd6409f,0,1},'
        '0,0,0,0,0,{},0,{},'
        '{0},0,0,0,0},'
        '"Процедура ПриОткрытии(Отказ)\nКонецПроцедуры\nПроцедура ПередЗагрузкойДанныхИзНастроекНаСервере(Настройки)\nКонецПроцедуры",'
        '{0,0},'
        '{0,0},'
        '{0,0},'
        '{0,0},{0,0},0,0}'
    )
    profile = decode_form_payload(parse_brace_text(text), max_items=20)
    events = {event["name"]: event for event in profile["events"]}

    assert events["OnOpen"]["handler"] == "ПриОткрытии"
    assert events["OnOpen"]["path"] == "1.18.2"
    assert events["BeforeLoadDataFromSettingsAtServer"]["handler"] == "ПередЗагрузкойДанныхИзНастроекНаСервере"
    assert "OnCreateAtServer" not in events


def test_decode_form_payload_exposes_table_additions_and_buttons() -> None:
    search_values = ["0"] * 25
    search_values[0] = "6"
    search_values[1] = "{40,00000000-0000-0000-0000-000000000024}"
    search_values[5] = "0"
    search_values[6] = '"ТЗСтрокаПоиска"'
    button_values = ["0"] * 60
    button_values[0] = "34"
    button_values[1] = "{40,00000000-0000-0000-0000-000000000057}"
    button_values[5] = "0"
    button_values[6] = '"ТЗИзменитьФорму"'
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{0},0,0,'
        "{"
        + ",".join(search_values)
        + "},{"
        + ",".join(button_values)
        + '},0},'
        '"",'
        '{0,0},'
        '{0,0},'
        '{0,0},'
        '{0,0},{0,0},0,0}'
    )
    profile = decode_form_payload(parse_brace_text(text), max_items=20)
    by_name = {item["name"]: item for item in profile["items"]}

    assert by_name["ТЗСтрокаПоиска"]["marker"] == "6"
    assert by_name["ТЗСтрокаПоиска"]["type_code"] == "0"
    assert by_name["ТЗСтрокаПоиска"]["type_name"] == "SearchStringAddition"
    assert by_name["ТЗИзменитьФорму"]["marker"] == "34"
    assert by_name["ТЗИзменитьФорму"]["type_name"] == "Кнопка"


def test_decode_form_payload_derives_child_item_references() -> None:
    table_values = ["0"] * 24
    table_values[0] = "73"
    table_values[1] = "{40,00000000-0000-0000-0000-000000000020}"
    table_values[6] = '"ТЗ"'
    command_bar_values = ["0"] * 24
    command_bar_values[0] = "22"
    command_bar_values[1] = "{40,00000000-0000-0000-0000-000000000022}"
    command_bar_values[5] = "9"
    command_bar_values[6] = '"ТЗКоманднаяПанель"'
    menu_values = ["0"] * 24
    menu_values[0] = "22"
    menu_values[1] = "{40,00000000-0000-0000-0000-000000000021}"
    menu_values[5] = "8"
    menu_values[6] = '"ТЗКонтекстноеМеню"'
    tooltip_values = ["0"] * 20
    tooltip_values[0] = "12"
    tooltip_values[1] = "{40,00000000-0000-0000-0000-000000000023}"
    tooltip_values[6] = '"ТЗРасширеннаяПодсказка"'
    search_values = ["0"] * 25
    search_values[0] = "6"
    search_values[1] = "{40,00000000-0000-0000-0000-000000000024}"
    search_values[5] = "0"
    search_values[6] = '"ТЗСтрокаПоиска"'
    table_values[13] = "{" + ",".join(command_bar_values) + "}"
    table_values[14] = "{" + ",".join(menu_values) + "}"
    table_values[15] = "{" + ",".join(tooltip_values) + "}"
    table_values[16] = "{" + ",".join(search_values) + "}"
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{0},0,0,'
        "{"
        + ",".join(table_values)
        + '},0},'
        '"",'
        '{0,0},'
        '{0,0},'
        '{0,0},'
        '{0,0},{0,0},0,0}'
    )
    profile = decode_form_payload(parse_brace_text(text), max_items=20)
    table = {item["name"]: item for item in profile["items"]}["ТЗ"]
    props = {prop["name"]: prop["value"] for props in table["semantic"]["groups"].values() for prop in props}

    assert props["AutoCommandBar"] == "ТЗКоманднаяПанель"
    assert props["КонтекстноеМеню"] == "ТЗКонтекстноеМеню"
    assert props["РасширеннаяПодсказка"] == "ТЗРасширеннаяПодсказка"
    assert props["SearchStringAddition"] == "ТЗСтрокаПоиска"


def test_decode_form_payload_exposes_section_kind_and_command_action() -> None:
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{0},0,0,0,0},'
        '"",'
        '{2,1,{9,{13},0,"Объект",{1,1,{"ru","Объект"}}}},'
        '{0,0},'
        '{2,1,{11,{13},0,"КомандаПрименить",{1,1,{"ru","Применить"}}}},'
        '{0,0},{0,0},0,0}'
    )
    profile = decode_form_payload(parse_brace_text(text), max_items=20)
    attribute_props = {prop["name"]: prop["value"] for props in profile["attributes"][0]["semantic"]["groups"].values() for prop in props}
    command_props = {prop["name"]: prop["value"] for props in profile["commands"][0]["semantic"]["groups"].values() for prop in props}

    assert attribute_props["Вид"] == "Attribute"
    assert attribute_props["MainAttribute"] is True
    assert command_props["Вид"] == "Command"
    assert command_props["Action"] == "КомандаПрименить"


def test_form_command_uses_explicit_action_parameter_for_handler_binding() -> None:
    row = {"category": "Command", "name": "КомандаФормы"}
    parameters = [
        {"index": 2, "value": "КомандаФормы"},
        {"index": 8, "value": "ВыполнитьКоманду"},
    ]

    semantic = section_record_semantic_properties(row, parameters)
    links = command_handler_links(
        [row],
        {"routines_sample": [{"name": "ВыполнитьКоманду"}]},
    )

    action = next(
        prop["value"]
        for prop in semantic["groups"]["Основные"]
        if prop["name"] == "Action"
    )
    assert action == "ВыполнитьКоманду"
    assert row["action"] == "ВыполнитьКоманду"
    assert row["action_source"] == "form_payload_parameter_8"
    assert semantic["coverage"]["mapped"] == 2
    assert links[0]["handler"] == "ВыполнитьКоманду"
    assert links[0]["status"] == "resolved"
    assert links[0]["match_by"] == "action"


def test_form_section_records_decode_saved_data_and_current_row_properties() -> None:
    command = section_record_semantic_properties(
        {"category": "Command", "name": "ЗаписатьИЗакрыть"},
        [
            {"index": 8, "value": "ЗаписатьИЗакрытьВыполнить"},
            {"index": 9, "value": "3"},
            {"index": 10, "value": "1"},
        ],
    )
    attribute = section_record_semantic_properties(
        {"category": "Attribute", "index": 1, "name": "ПредставлениеФайлаОтвета"},
        [
            {"index": 10, "value": "0"},
            {"index": 11, "value": "1"},
        ],
    )
    command_props = {prop["name"]: prop for props in command["groups"].values() for prop in props}
    attribute_props = {prop["name"]: prop for props in attribute["groups"].values() for prop in props}

    assert command_props["CurrentRowUse"]["value"] == "DontUse"
    assert command_props["CurrentRowUse"]["parameter_index"] == 9
    assert command_props["ModifiesSavedData"]["value"] is True
    assert command_props["ModifiesSavedData"]["parameter_index"] == 10
    assert attribute_props["MainAttribute"]["value"] is False
    assert attribute_props["SavedData"]["value"] is True
    assert attribute_props["SavedData"]["parameter_index"] == 11


def test_form_section_semantic_title_uses_decoded_title_path_when_direct_parameter_is_empty() -> None:
    semantic = section_record_semantic_properties(
        {"category": "Command", "name": "Команда", "title": "Показать продажи"},
        [{"index": 3, "value": ""}],
    )
    properties = {prop["name"]: prop for group in semantic["groups"].values() for prop in group}

    assert properties["Заголовок"]["value"] == "Показать продажи"
    assert properties["Заголовок"]["source"] == "form_payload_title_path"


def test_form_table_additions_expose_structured_addition_source() -> None:
    records = [
        {"name": "ТЗ", "marker": "73", "type_name": "Таблица формы", "path": "1.25.10", "semantic": {"groups": {}}},
        {"name": "ТЗСтрокаПоиска", "marker": "6", "type_name": "SearchStringAddition", "path": "1.25.10.20", "semantic": {"groups": {}}},
    ]

    enrich_item_reference_semantics(records)

    source = records[1]["addition_source"]
    props = {prop["name"]: prop["value"] for props in records[1]["semantic"]["groups"].values() for prop in props}
    assert source == {"owner_element": "ТЗ", "representation": "SearchStringRepresentation"}
    assert props["AdditionSource"] == source


def test_embedded_dynamic_list_tail_addition_is_normalized_as_public_item() -> None:
    node = parse_brace_text(
        '{5,{7,02023637-7868-4a5f-8576-835a76e0c9ba},0,0,0,0,"СписокСтрокаПоиска",'
        '{1,0},{1,0},0,0,0,1,{},1,{},1,{},2,{3,0},0,3,3,0}'
    )

    row = embedded_table_addition_row(
        node,
        "1.27.116",
        2,
        include_parameters=True,
        max_parameters=100,
    )

    assert row is not None
    assert row["marker"] == "6"
    assert row["type_code"] == "0"
    assert row["type_name"] == "SearchStringAddition"
    assert row["name"] == "СписокСтрокаПоиска"
    assert row["id"] == "7"
    props = {prop["name"]: prop for props in row["semantic"]["groups"].values() for prop in props}
    assert props["Видимость"]["value"] is False
    assert props["Доступность"]["value"] is False


def test_form_command_reports_missing_explicit_action_handler() -> None:
    row = {"category": "Command", "name": "КомандаФормы"}
    section_record_semantic_properties(row, [{"index": 8, "value": "НеНайденныйОбработчик"}])

    link = command_handler_links([row], {"routines_sample": []})[0]

    assert link["status"] == "missing"
    assert link["match_by"] == "action"


def test_form_command_handler_resolution_uses_all_module_routine_names() -> None:
    row = {"category": "Command", "name": "КомандаФормы"}
    section_record_semantic_properties(row, [{"index": 8, "value": "ПозднийОбработчик"}])
    module = {
        "routine_count": 81,
        "routine_names": [*[f"Процедура{index}" for index in range(80)], "ПозднийОбработчик"],
        "routines_sample": [{"name": f"Процедура{index}"} for index in range(80)],
    }

    link = command_handler_links([row], module)[0]

    assert link["status"] == "resolved"
    assert link["handler"] == "ПозднийОбработчик"


def test_decode_form_payload_exposes_marker_48_field_details() -> None:
    details_values = ["0"] * 71
    details_values[0] = "38"
    details_values[2] = "5"
    details_values[3] = "8"
    details_values[8] = "1"
    details_values[49] = "0"
    field_values = ["0"] * 75
    field_values[0] = "48"
    field_values[1] = "{40,00000000-0000-0000-0000-000000000012}"
    field_values[6] = "2"
    field_values[7] = '"КодПрограммы"'
    field_values[40] = "{" + ",".join(details_values) + "}"
    field_values[62] = "3"
    field_values[63] = "1"
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{0},0,0,'
        "{"
        + ",".join(field_values)
        + '},0},'
        '"",'
        '{0,0},'
        '{0,0},'
        '{0,0},'
        '{0,0},{0,0},0,0}'
    )
    profile = decode_form_payload(parse_brace_text(text), max_items=20)
    props = {prop["name"]: prop["value"] for props in profile["items"][0]["semantic"]["groups"].values() for prop in props}

    assert props["Ширина"] == "5"
    assert props["Высота"] == "8"
    assert props["АвтоМаксимальнаяШирина"] is False
    assert props["MultiLine"] is True
    assert props["РежимРедактирования"] == "EnterOnInput"
    assert props["AutoEditMode"] is True


def test_decode_form_payload_exposes_group_layout_semantics() -> None:
    group_values = ["0"] * 35
    group_values[0] = "22"
    group_values[1] = "{40,00000000-0000-0000-0000-000000000067}"
    group_values[5] = '""'
    group_values[6] = "5"
    group_values[7] = '"ГруппаГоризонтальная"'
    group_values[10] = "0"
    group_values[22] = "2"
    group_values[23] = "cd5394d0-7dda-4b56-8927-93ccbe967a01"
    group_values[25] = "cd5394d0-7dda-4b56-8927-93ccbe967a01"
    group_values[27] = "1"
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{0},0,0,'
        "{"
        + ",".join(group_values)
        + '},0},'
        '"",'
        '{0,0},'
        '{0,0},'
        '{0,0},'
        '{0,0},{0,0},0,0}'
    )
    profile = decode_form_payload(parse_brace_text(text), max_items=20)
    props = {prop["name"]: prop["value"] for props in profile["items"][0]["semantic"]["groups"].values() for prop in props}

    assert props["Группировка"] == "AlwaysHorizontal"
    assert props["Поведение"] == "Usual"
    assert props["Отображение"] == "None"
    assert props["ПоказыватьЗаголовок"] is False


def test_decode_form_payload_reads_live_group_layout_slots() -> None:
    def group_props(values: list[str]) -> dict[str, object]:
        text = (
            '{4,'
            '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
            '{0},0,0,'
            "{"
            + ",".join(values)
            + '},0},'
            '"",'
            '{0,0},'
            '{0,0},'
            '{0,0},'
            '{0,0},{0,0},0,0}'
        )
        profile = decode_form_payload(parse_brace_text(text), max_items=20)
        return {prop["name"]: prop for props in profile["items"][0]["semantic"]["groups"].values() for prop in props}

    vertical_values = ["0"] * 35
    vertical_values[0] = "22"
    vertical_values[1] = "{40,00000000-0000-0000-0000-000000000065}"
    vertical_values[5] = "5"
    vertical_values[6] = '"ГруппаФорма"'
    vertical_values[10] = "1"
    vertical_values[21] = "2"
    vertical_values[22] = "cd5394d0-7dda-4b56-8927-93ccbe967a01"
    vertical_values[24] = "143c00f7-a42d-4cd7-9189-88e4467dc768"
    vertical_values[26] = "1"
    vertical_props = group_props(vertical_values)

    horizontal_values = ["0"] * 35
    horizontal_values[0] = "22"
    horizontal_values[1] = "{40,00000000-0000-0000-0000-000000000001}"
    horizontal_values[5] = "5"
    horizontal_values[6] = '"Группа1"'
    horizontal_values[10] = "1"
    horizontal_values[21] = "3"
    horizontal_values[22] = "77ffcc29-7f2d-4223-b22f-19666e7250ba"
    horizontal_values[24] = "77ffcc29-7f2d-4223-b22f-19666e7250ba"
    horizontal_values[26] = "77ffcc29-7f2d-4223-b22f-19666e7250ba"
    horizontal_props = group_props(horizontal_values)

    assert vertical_props["Группировка"]["value"] == "Vertical"
    assert vertical_props["Группировка"]["parameter_index"] == 21
    assert vertical_props["Отображение"]["value"] == "WeakSeparation"
    assert vertical_props["Отображение"]["parameter_index"] == 24
    assert horizontal_props["Группировка"]["value"] == "HorizontalIfPossible"
    assert horizontal_props["Отображение"]["value"] == "None"
    assert horizontal_props["ПоказыватьЗаголовок"]["value"] is False


def test_decode_form_payload_exposes_table_layout_semantics() -> None:
    table_values = ["0"] * 60
    table_values[0] = "73"
    table_values[1] = "{40,00000000-0000-0000-0000-000000000020}"
    table_values[6] = '"ТЗ"'
    table_values[31] = "2"
    table_values[33] = "0"
    table_values[34] = "0"
    table_values[35] = "0"
    table_values[39] = "2"
    table_values[42] = "1"
    table_values[43] = "1"
    table_values[53] = "1"
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{0},0,0,'
        "{"
        + ",".join(table_values)
        + '},0},'
        '"",'
        '{0,0},'
        '{0,0},'
        '{0,0},'
        '{0,0},{0,0},0,0}'
    )
    profile = decode_form_payload(parse_brace_text(text), max_items=20)
    props = {prop["name"]: prop["value"] for props in profile["items"][0]["semantic"]["groups"].values() for prop in props}

    assert props["Отображение"] == "List"
    assert props["HeightInTableRows"] == "2"
    assert props["RowSelectionMode"] == "Cell"
    assert props["HorizontalLinesBWA"] is False
    assert props["VerticalLinesBWA"] is False
    assert props["UseAlternationRowColorBWA"] is False
    assert props["AutoInsertNewRow"] is True
    assert props["EnableStartDrag"] is True
    assert props["EnableDrag"] is True


def test_decode_form_payload_exposes_dynamic_table_confirmed_semantics() -> None:
    table_values = ["0"] * 60
    table_values[0] = "55"
    table_values[1] = "{40,00000000-0000-0000-0000-000000000020}"
    table_values[5] = '"РаботыИУслуги"'
    table_values[13] = "1"
    table_values[14] = "1"
    table_values[15] = "0"
    table_values[17] = "0"
    table_values[21] = "5"
    table_values[28] = "1"
    table_values[31] = "2"
    table_values[33] = "1"
    table_values[34] = "0"
    table_values[35] = "0"
    table_values[41] = "1"
    table_values[42] = "1"
    table_values[53] = "1"
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{0},0,0,'
        "{"
        + ",".join(table_values)
        + '},0},'
        '"",'
        '{0,0},'
        '{0,0},'
        '{0,0},'
        '{0,0},{0,0},0,0}'
    )

    profile = decode_form_payload(parse_brace_text(text), max_items=20)
    props = {prop["name"]: prop["value"] for group in profile["items"][0]["semantic"]["groups"].values() for prop in group}

    assert props["Вид"] == "Динамический список"
    assert props["ТолькоПросмотр"] is True
    assert props["ПропускатьПриВводе"] is False
    assert props["ChangeRowSet"] is False
    assert props["Отображение"] == "List"
    assert props["HeightInTableRows"] == "5"
    assert props["Footer"] is True
    assert props["RowSelectionMode"] == "Cell"
    assert props["HorizontalLinesBWA"] is True
    assert props["VerticalLinesBWA"] is False
    assert props["UseAlternationRowColorBWA"] is False
    assert props["AutoInsertNewRow"] is True
    assert props["EnableStartDrag"] is True
    assert props["EnableDrag"] is True


def test_decode_form_payload_exposes_dynamic_table_relative_tail_locations() -> None:
    table_values = ["0"] * 90
    table_values[0] = "55"
    table_values[1] = "{40,00000000-0000-0000-0000-000000000020}"
    table_values[5] = '"Список"'
    table_values[64] = "{12,0}"
    table_values[65] = "6"
    table_values[66] = "0"
    table_values[67] = "2"
    table_values[69] = "{5,0}"
    table_values[71] = "{5,0}"
    table_values[73] = "{5,0}"
    table_values[88] = "0"
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{0},0,0,'
        "{"
        + ",".join(table_values)
        + '},0},'
        '"",'
        '{0,0},'
        '{0,0},'
        '{0,0},'
        '{0,0},{0,0},0,0}'
    )

    profile = decode_form_payload(parse_brace_text(text), max_items=20)
    props = {prop["name"]: prop for group in profile["items"][0]["semantic"]["groups"].values() for prop in group}

    assert props["SearchStringLocation"]["value"] == "PullFromTop"
    assert props["SearchStringLocation"]["parameter_index"] == 65
    assert props["ViewStatusLocation"]["value"] == "Default"
    assert props["SearchControlLocation"]["value"] == "CommandBar"
    assert props["FileDragMode"]["value"] == "AsFile"
    assert props["FileDragMode"]["parameter_index"] == 88


def test_dynamic_list_decodes_live_property_bag_and_references() -> None:
    from parser.form_payload import enrich_dynamic_list_specific_semantics, enrich_item_reference_semantics

    values = ["0"] * 78
    values[0] = "55"
    values[5] = '"Список"'
    values[6] = "0"
    values[13] = "1"
    values[16] = "1"
    values[54] = "10"
    pairs = {
        55: ("5", '{"B",0}'),
        57: ("6", '{"N",60}'),
        59: ("7", '{"#",2fdc88ec-7c9b-43cd-8ba5-873f043bdd88,{0,00010101000000,00010101000000}}'),
        61: ("8", '{"#",59ef2b80-c86b-11d5-a3c1-0050bae0a776,0}'),
        63: ("9", '{"B",0}'),
        65: ("10", '{"U"}'),
        67: ("11", '{"B",1}'),
        69: ("12", '{"B",0}'),
        71: ("14", '{"#",eac7bfa0-10b4-4369-996c-d258871ad519,0}'),
        73: ("16", '{"U"}'),
    }
    for index, (key, value) in pairs.items():
        values[index] = key
        values[index + 1] = value
    values[75] = "{0,1,0}"
    values[76] = "{0}"
    values[77] = "1"
    node = parse_brace_text("{" + ",".join(values) + "}")
    row = {
        "marker": "55",
        "name": "Список",
        "path": "1.27",
        "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}},
    }

    enrich_dynamic_list_specific_semantics(row, node)
    enrich_item_reference_semantics([
        {"id": "1", "name": "НастройкиПользователя", "path": "1.25", "semantic": {"groups": {}}},
        row,
    ])
    props = {prop["name"]: prop for group in row["semantic"]["groups"].values() for prop in group}

    assert props["ПоложениеКоманднойПанели"]["value"] == "None"
    assert props["АктивизироватьПоУмолчанию"]["value"] is True
    assert props["AutoRefresh"]["value"] is False
    assert props["AutoRefreshPeriod"]["value"] == "60"
    assert props["ChoiceFoldersAndItems"]["value"] == "Items"
    assert props["RestoreCurrentRow"]["value"] is False
    assert props["ShowRoot"]["value"] is True
    assert props["AllowRootChoice"]["value"] is False
    assert props["UpdateOnDataChange"]["value"] == "Auto"
    assert props["AllowGettingCurrentRowURL"]["value"] is True
    assert props["InitialTreeView"]["value"] == "ExpandTopLevel"
    assert props["RowPictureDataPath"]["value"] == "Список.DefaultPicture"
    assert props["UserSettingsGroup"]["value"] == "НастройкиПользователя"


def test_compact_command_bar_and_label_field_tail_properties() -> None:
    from parser.form_payload import enrich_container_specific_semantics, enrich_input_field_specific_semantics

    command_values = ["0"] * 29
    command_values[0] = "22"
    command_values[5] = "9"
    command_values[6] = '"КоманднаяПанель"'
    command_values[10] = "1"
    command_values[28] = "0"
    command_row = {"marker": "22", "type_name": "Командная панель", "semantic": {"groups": {}}}
    enrich_container_specific_semantics(command_row, parse_brace_text("{" + ",".join(command_values) + "}"))

    field_values = ["0"] * 57
    field_values[0] = "35"
    field_values[5] = "1"
    field_values[6] = '"ДатаСоздания"'
    field_values[39] = "{11,0,0,2,2,2,{1,0},0,{0},{0},{0},2,{0,1,0},{0},{0},0,8,0,1,0}"
    field_row = {"marker": "35", "type_code": "1", "semantic": {"groups": {}}}
    enrich_input_field_specific_semantics(field_row, parse_brace_text("{" + ",".join(field_values) + "}"))

    command_props = {prop["name"]: prop for group in command_row["semantic"]["groups"].values() for prop in group}
    field_props = {prop["name"]: prop for group in field_row["semantic"]["groups"].values() for prop in group}
    assert command_props["Автозаполнение"]["value"] is False
    assert command_props["Автозаполнение"]["parameter_index"] == 28
    assert field_props["АвтоМаксимальнаяШирина"]["value"] is False
    assert field_props["МаксимальнаяШирина"]["value"] == "8"
    assert field_props["МаксимальнаяШирина"]["parameter_index"] == 39


def test_compact_base_group_decodes_vertical_weak_separation_without_title() -> None:
    from parser.form_payload import enrich_container_specific_semantics

    values = ["0"] * 30
    values[0] = "22"
    values[5] = "5"
    values[6] = '"НастройкиПользователя"'
    values[10] = "1"
    values[21] = "0"
    values[22] = "1"
    values[24] = "1"
    row = {"marker": "22", "type_name": "Группа", "semantic": {"groups": {}}}

    enrich_container_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))
    props = {prop["name"]: prop["value"] for group in row["semantic"]["groups"].values() for prop in group}

    assert props["Группировка"] == "Vertical"
    assert props["Отображение"] == "WeakSeparation"
    assert props["ПоказыватьЗаголовок"] is False


@pytest.mark.parametrize(
    ("options", "expected_group", "expected_representation"),
    [
        ("{29,0,0,0,0}", "Vertical", "None"),
        ("{29,1,0,0,0}", "AlwaysHorizontal", "None"),
        ("{29,1,0,2,1}", "HorizontalIfPossible", "WeakSeparation"),
        ("{29,1,0,2,0,{0},{1,0},{0},0,{0},0,0,0,1,{1,0},0,0,3,3,2,0,0,2}", "HorizontalIfPossible", "WeakSeparation"),
        ("{29,0,0,3,0}", "Vertical", "NormalSeparation"),
    ],
)
def test_managed_group_options_decode_grouping_and_representation(
    options: str,
    expected_group: str,
    expected_representation: str,
) -> None:
    from parser.form_payload import enrich_container_specific_semantics

    values = ["0"] * 30
    values[0] = "22"
    values[5] = "5"
    values[6] = '"Группа"'
    values[10] = "1"
    values[20] = options
    row = {"marker": "22", "type_name": "Группа", "semantic": {"groups": {}}}

    enrich_container_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))
    props = {prop["name"]: prop["value"] for group in row["semantic"]["groups"].values() for prop in group}
    assert props["Группировка"] == expected_group
    assert props["Отображение"] == expected_representation
    assert props["ПоказыватьЗаголовок"] is False


def test_report_page_and_managed_group_decode_horizontal_layout() -> None:
    from parser.form_payload import enrich_container_specific_semantics

    page_values = ["0"] * 30
    page_values[0] = "22"
    page_values[5] = "6"
    page_values[10] = "1"
    page_values[20] = '{18,{3,4,{0}},1,0,{0},{1,0},1,{"Pattern"},"",{3,4,{0}},0,0,3,3,0,0,1}'
    page_row = {"marker": "22", "type_name": "Страница", "semantic": {"groups": {}}}
    enrich_container_specific_semantics(page_row, parse_brace_text("{" + ",".join(page_values) + "}"))

    options = ["0"] * 28
    options[0] = "29"
    options[1] = "1"
    options[2] = "5"
    options[27] = "1"
    group_values = ["0"] * 30
    group_values[0] = "22"
    group_values[5] = "5"
    group_values[10] = "1"
    group_values[20] = "{" + ",".join(options) + "}"
    group_row = {"marker": "22", "type_name": "Группа", "semantic": {"groups": {}}}
    enrich_container_specific_semantics(group_row, parse_brace_text("{" + ",".join(group_values) + "}"))

    page_props = {prop["name"]: prop["value"] for group in page_row["semantic"]["groups"].values() for prop in group}
    group_props = {prop["name"]: prop["value"] for group in group_row["semantic"]["groups"].values() for prop in group}
    assert page_props["Группировка"] == "Horizontal"
    assert group_props["Группировка"] == "Horizontal"
    assert group_props["ChildItemsWidth"] == "LeftNarrowest"


def test_command_bar_decodes_form_command_source_guid() -> None:
    from parser.form_payload import enrich_container_specific_semantics

    values = ["0"] * 32
    values[0] = "22"
    values[5] = "9"
    values[10] = "1"
    values[22] = "a9f3b1ac-f51b-431e-b102-55a69acdecad"
    row = {"marker": "22", "type_name": "Командная панель", "semantic": {"groups": {}}}

    enrich_container_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))

    props = {prop["name"]: prop["value"] for group in row["semantic"]["groups"].values() for prop in group}
    assert props["CommandSource"] == "Form"


def test_button_group_decodes_form_command_source() -> None:
    from parser.form_payload import enrich_container_specific_semantics

    values = ["0"] * 30
    values[0] = "22"
    values[5] = "6"
    values[10] = "1"
    values[20] = "{2,{0,02023637-7868-4a5f-8576-835a76e0c9ba},2,0}"
    row = {"marker": "22", "type_name": "Группа кнопок", "semantic": {"groups": {}}}

    enrich_container_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))

    props = {prop["name"]: prop["value"] for group in row["semantic"]["groups"].values() for prop in group}
    assert props["CommandSource"] == "Form"


@pytest.mark.parametrize(("mode", "expected"), [("0", "Horizontal"), ("2", "InCell")])
def test_column_group_decodes_layout(mode: str, expected: str) -> None:
    from parser.form_payload import enrich_container_specific_semantics

    values = ["0"] * 38
    values[0] = "22"
    values[5] = "2"
    values[10] = "1"
    values[14] = "1"
    values[20] = "{2," + mode + ",1,0,3}"
    row = {"marker": "22", "type_name": "Группа колонок", "semantic": {"groups": {}}}

    enrich_container_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))

    props = {prop["name"]: prop["value"] for group in row["semantic"]["groups"].values() for prop in group}
    assert props["Группировка"] == expected
    assert props["РастягиватьПоГоризонтали"] is True


def test_managed_group_decodes_width_united_and_background() -> None:
    from parser.form_payload import enrich_container_specific_semantics

    values = ["0"] * 34
    values[0] = "22"
    values[5] = "5"
    values[6] = '"Группа"'
    values[10] = "1"
    values[12] = "24"
    values[20] = "{29,1,0,0,0,{0},{1,0},{0},0,{3,3,{-35}},0,0,0,1,{1,0},0,0,3,3,2,0,0,1}"
    row = {"marker": "22", "type_name": "Группа", "semantic": {"groups": {}}}

    enrich_container_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))

    props = {item["name"]: item["value"] for items in row["semantic"]["groups"].values() for item in items}
    assert props["Ширина"] == "24"
    assert props["United"] is False
    assert props["ЦветФона"] == "style:TableHeaderBackColor"


def test_compact_typed_group_decodes_horizontal_if_possible_from_secondary_slot() -> None:
    from parser.form_payload import enrich_container_specific_semantics

    values = ["0"] * 34
    values[0] = "22"
    values[5] = "5"
    values[6] = '"ГруппаВариант"'
    values[10] = "1"
    values[12] = "0"
    values[21] = "2"
    values[22] = "3d3cb80c-508b-41fa-8a18-680cdf5f1712"
    values[24] = "77ffcc29-7f2d-4223-b22f-19666e7250ba"
    row = {"marker": "22", "type_name": "Группа", "semantic": {"groups": {}}}

    enrich_container_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))

    props = {prop["name"]: prop["value"] for group in row["semantic"]["groups"].values() for prop in group}
    assert props["Группировка"] == "HorizontalIfPossible"
    assert props["Отображение"] == "WeakSeparation"


def test_decode_form_payload_reads_table_height_from_live_marker_73_slot() -> None:
    table_values = ["0"] * 60
    table_values[0] = "73"
    table_values[1] = "{40,00000000-0000-0000-0000-000000000020}"
    table_values[5] = '"ТЗ"'
    table_values[38] = "2"
    table_values[39] = "0"
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{0},0,0,'
        "{"
        + ",".join(table_values)
        + '},0},'
        '"",'
        '{0,0},'
        '{0,0},'
        '{0,0},'
        '{0,0},{0,0},0,0}'
    )

    profile = decode_form_payload(parse_brace_text(text), max_items=20)
    props = {prop["name"]: prop for props in profile["items"][0]["semantic"]["groups"].values() for prop in props}

    assert props["HeightInTableRows"]["value"] == "2"
    assert props["HeightInTableRows"]["parameter_index"] == 38


def test_decode_form_payload_reads_live_marker_48_behavior_slots() -> None:
    field_values = ["0"] * 74
    field_values[0] = "48"
    field_values[1] = "{3,00000000-0000-0000-0000-000000000003}"
    field_values[5] = "2"
    field_values[6] = '"КодПрограммы"'
    field_values[7] = "0"
    field_values[13] = "0"
    field_values[14] = "1"
    field_values[15] = "1"
    field_values[43] = "0"
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{0},0,0,'
        "{"
        + ",".join(field_values)
        + '},0},'
        '"",'
        '{0,0},'
        '{0,0},'
        '{0,0},'
        '{0,0},{0,0},0,0}'
    )

    profile = decode_form_payload(parse_brace_text(text), max_items=20)
    props = {prop["name"]: prop for props in profile["items"][0]["semantic"]["groups"].values() for prop in props}

    assert props["ПоложениеЗаголовка"]["value"] == "None"
    assert props["ПоложениеЗаголовка"]["parameter_index"] == 7
    assert props["Видимость"]["value"] is False
    assert props["Видимость"]["parameter_index"] == 43
    assert props["Доступность"]["value"] is False
    assert props["Доступность"]["parameter_index"] == 13
    assert props["ТолькоПросмотр"]["value"] is True
    assert props["ТолькоПросмотр"]["parameter_index"] == 14
    assert props["ПропускатьПриВводе"]["value"] is True
    assert props["ПропускатьПриВводе"]["parameter_index"] == 15


def test_decode_form_payload_reads_marker_34_button_visibility_and_availability() -> None:
    button_values = ["0"] * 56
    button_values[0] = "34"
    button_values[1] = "{15,00000000-0000-0000-0000-000000000015}"
    button_values[5] = '"ФормаКомандаОбновить"'
    button_values[7] = "0"
    button_values[8] = "{1,409b9a53-7f7e-4178-86c1-33176c7c7a7a}"
    button_values[26] = "0"
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{0},0,0,'
        "{"
        + ",".join(button_values)
        + '},0},'
        '"",'
        '{0,0},'
        '{0,0},'
        '{0,0},'
        '{0,0},{0,0},0,0}'
    )

    profile = decode_form_payload(parse_brace_text(text), max_items=20)
    props = {prop["name"]: prop for props in profile["items"][0]["semantic"]["groups"].values() for prop in props}

    assert props["Видимость"]["value"] is False
    assert props["Видимость"]["parameter_index"] == 26
    assert props["Доступность"]["value"] is False
    assert props["Доступность"]["parameter_index"] == 7


def test_decode_form_payload_reads_live_container_and_table_behavior_slots() -> None:
    def profile_props(values: list[str]) -> dict[str, dict[str, object]]:
        text = (
            '{4,'
            '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
            '{0},0,0,'
            "{"
            + ",".join(values)
            + '},0},'
            '"",'
            '{0,0},'
            '{0,0},'
            '{0,0},'
            '{0,0},{0,0},0,0}'
        )
        profile = decode_form_payload(parse_brace_text(text), max_items=20)
        return {prop["name"]: prop for props in profile["items"][0]["semantic"]["groups"].values() for prop in props}

    vertical_group = ["0"] * 36
    vertical_group[0] = "22"
    vertical_group[1] = "{65,00000000-0000-0000-0000-000000000065}"
    vertical_group[5] = "5"
    vertical_group[6] = '"ГруппаФорма"'
    vertical_group[10] = "1"
    vertical_group[21] = "2"
    vertical_group[22] = "cd5394d0-7dda-4b56-8927-93ccbe967a01"
    vertical_group[24] = "143c00f7-a42d-4cd7-9189-88e4467dc768"
    vertical_group[26] = "0"

    horizontal_group = ["0"] * 36
    horizontal_group[0] = "22"
    horizontal_group[1] = "{1,00000000-0000-0000-0000-000000000001}"
    horizontal_group[5] = "5"
    horizontal_group[6] = '"Группа1"'
    horizontal_group[10] = "1"
    horizontal_group[21] = "3"
    horizontal_group[22] = "77ffcc29-7f2d-4223-b22f-19666e7250ba"
    horizontal_group[24] = "77ffcc29-7f2d-4223-b22f-19666e7250ba"
    horizontal_group[26] = "77ffcc29-7f2d-4223-b22f-19666e7250ba"
    horizontal_group[28] = "0"

    table_values = ["0"] * 74
    table_values[0] = "73"
    table_values[1] = "{20,00000000-0000-0000-0000-000000000020}"
    table_values[5] = '"ТЗ"'
    table_values[13] = "0"
    table_values[38] = "2"
    table_values[72] = "0"

    vertical_props = profile_props(vertical_group)
    horizontal_props = profile_props(horizontal_group)
    table_props = profile_props(table_values)

    assert vertical_props["Видимость"]["value"] is False
    assert vertical_props["Видимость"]["parameter_index"] == 26
    assert horizontal_props["Видимость"]["value"] is False
    assert horizontal_props["Видимость"]["parameter_index"] == 28
    assert table_props["Видимость"]["value"] is False
    assert table_props["Видимость"]["parameter_index"] == 72
    assert table_props["Доступность"]["value"] is False
    assert table_props["Доступность"]["parameter_index"] == 13


def test_decode_form_payload_reads_marker_6_table_addition_behavior_slots() -> None:
    addition_values = ["0"] * 24
    addition_values[0] = "6"
    addition_values[1] = "{24,00000000-0000-0000-0000-000000000024}"
    addition_values[5] = "2"
    addition_values[6] = '"ТЗУправлениеПоиском"'
    addition_values[9] = "0"
    addition_values[10] = "0"
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{0},0,0,'
        "{"
        + ",".join(addition_values)
        + '},0},'
        '"",'
        '{0,0},'
        '{0,0},'
        '{0,0},'
        '{0,0},{0,0},0,0}'
    )

    profile = decode_form_payload(parse_brace_text(text), max_items=20)
    props = {prop["name"]: prop for props in profile["items"][0]["semantic"]["groups"].values() for prop in props}

    assert props["Видимость"]["value"] is False
    assert props["Видимость"]["parameter_index"] == 9
    assert props["Доступность"]["value"] is False
    assert props["Доступность"]["parameter_index"] == 10


def test_form_sql_xml_compare_matches_columns_by_name_before_id() -> None:
    sql_form = {
        "profile": {
            "items": [
                {"name": "ДругаяГруппа", "id": "1", "type_name": "Группа"},
                {"name": "К1", "id": "1", "type_name": "Колонка динамического списка"},
            ]
        }
    }
    xml_profile = {"items": [{"kind": "Column", "name": "К1", "id": "1", "type_name": "Column"}]}

    result = form_sql_xml_compare_script.compare_form(sql_form, xml_profile, sample_limit=10)

    assert result["counts"]["matched_items"] == 1
    assert result["counts"]["matched_properties"] >= 3
    assert result["counts"].get("mismatches", 0) == 0
    assert result["samples"]["matched"][0]["sql_type"] == "Колонка динамического списка"


def test_form_sql_xml_compare_reports_root_form_semantics() -> None:
    sql_form = {
        "profile": {
            "form_parameters": [
                {"index": 17, "value": "0", "value_kind": "boolean_or_number"},
                {"index": 56, "value": "0", "value_kind": "boolean_or_number"},
            ]
        }
    }
    xml_profile = decode_form_xml(
        "<Form><WindowOpeningMode>DontBlock</WindowOpeningMode><ShowCommandBar>false</ShowCommandBar></Form>"
    )

    result = form_sql_xml_compare_script.compare_form(sql_form, xml_profile, sample_limit=10)

    assert result["counts"]["matched_form_properties"] == 2
    assert result["counts"]["xml_only_form_properties"] == 1
    assert result["samples"]["xml_only_form_property"] == [
        {"property": "РежимОткрытияОкна", "xml_name": "WindowOpeningMode", "xml_value": "DontBlock"}
    ]
    assert result["form_property_routes"] == [
        {
            "xml_name": "ShowCommandBar",
            "property": "ОтображатьКоманднуюПанель",
            "parameter_indices": [17, 56],
            "sql_source": "controlled_designer_form_show_command_bar",
            "write_shape": "paired_scalar",
        }
    ]


def test_form_sql_xml_compare_matches_xml_button_to_command_by_name() -> None:
    sql_form = {"profile": {"commands": [{"name": "КомандаПример1", "id": "2", "title": "Пример1"}]}}
    xml_profile = {"items": [{"kind": "Button", "name": "КомандаПример1", "id": "61", "title": "Пример1"}]}

    result = form_sql_xml_compare_script.compare_form(sql_form, xml_profile, sample_limit=10)

    assert result["counts"]["matched_items"] == 1
    assert result["counts"].get("mismatches", 0) == 0
    assert result["samples"]["matched"][0]["name"] == "КомандаПример1"


def test_form_sql_xml_compare_reports_command_name_matches() -> None:
    sql_form = {
        "profile": {
            "items": [
                {
                    "name": "Кнопка",
                    "id": "105",
                    "path": "1.22.23",
                    "semantic": {
                        "groups": {
                            "Команда": [
                                {
                                    "name": "ИмяКоманды",
                                    "value": "Form.StandardCommand.CustomizeForm",
                                    "source": "button_command_link:standard_command_guid",
                                    "parameter_index": 8,
                                }
                            ]
                        }
                    },
                }
            ]
        }
    }
    xml_profile = {
        "items": [
            {
                "kind": "Button",
                "name": "Кнопка",
                "id": "105",
                "semantic": {
                    "groups": {
                        "Основные": [
                            {
                                "name": "ИмяКоманды",
                                "xml_name": "CommandName",
                                "value": "Form.StandardCommand.CustomizeForm",
                            }
                        ]
                    }
                },
            }
        ]
    }

    result = form_sql_xml_compare_script.compare_form(sql_form, xml_profile, sample_limit=10)

    assert result["counts"]["command_name_matches"] == 1
    assert result["counts"].get("command_name_mismatches", 0) == 0
    assert result["samples"]["command_name_match"][0]["item"] == "Кнопка"
    assert result["property_routes"] == [
        {
            "xml_kind": "Button",
            "xml_name": "CommandName",
            "property": "ИмяКоманды",
            "sql_marker": None,
            "parameter_index": 8,
            "sql_source": "button_command_link:standard_command_guid",
            "matches": 1,
        }
    ]


def test_form_sql_xml_compare_matches_nested_element_events() -> None:
    sql_form = {
        "profile": {
            "items": [
                {
                    "name": "КодПрограммы",
                    "id": "12",
                    "events": [
                        {
                            "event_name": "EditTextChange",
                            "handler": "КодПрограммыИзменениеТекстаРедактирования",
                            "guid": "14256303-d2b7-4a58-bfab-e77493d10a59",
                        }
                    ],
                }
            ]
        }
    }
    xml_profile = {
        "items": [
            {
                "kind": "Event",
                "name": "EditTextChange",
                "type_name": "Событие",
                "semantic": {
                    "groups": {
                        "Основные": [
                            {"name": "Обработчик", "xml_name": "Handler", "value": "КодПрограммыИзменениеТекстаРедактирования"}
                        ]
                    }
                },
            }
        ]
    }

    result = form_sql_xml_compare_script.compare_form(sql_form, xml_profile, sample_limit=10)

    assert result["counts"]["matched_items"] == 1
    assert result["counts"].get("missing_sql_item", 0) == 0
    assert result["counts"].get("mismatches", 0) == 0
    assert result["counts"]["matched_properties"] >= 3


def test_form_sql_xml_compare_links_additional_column_to_sql_field_data_path() -> None:
    sql_form = {
        "profile": {
            "items": [
                {
                    "name": "СтрокиЗначение",
                    "id": "108",
                    "type_name": "Поле ввода",
                    "path": "1.20.30",
                    "path_to_data": "Объект.Строки.Значение",
                }
            ]
        }
    }
    xml_profile = {
        "items": [
            {
                "kind": "Column",
                "kind_ru": "Колонка реквизита",
                "name": "Значение",
                "id": "1",
                "additional_columns_table": "Объект.Строки",
                "semantic": {"groups": {}},
            }
        ]
    }

    result = form_sql_xml_compare_script.compare_form(sql_form, xml_profile, sample_limit=10)

    assert result["counts"]["matched_items"] == 1
    assert result["counts"]["logical_column_sql_match"] == 1
    assert result["counts"].get("missing_sql_item", 0) == 0


def test_decode_form_payload_links_marker_34_buttons_to_commands_by_name() -> None:
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{0},0,0,'
        '{34,{40,00000000-0000-0000-0000-000000000061},0,1,{},1,"КомандаПример1",{},1,{}, {},3,0,0,0,2,2,0,0,0,{},{},{},{},{},0,{},1,{}, "",2,0,1,{}, {},1,0,0,1,0,0,0,3,3,3,0,0,1,0,0,0,1,0,0,"",0,0,0,1,{}},0},'
        '"Процедура КомандаПример1(Команда)\nКонецПроцедуры",'
        '{0,0},'
        '{0,0},'
        '{2,1,{11,{13},0,"КомандаПример1",{1,1,{"ru","Пример1"}}}},'
        '{0,0},{0,0},0,0}'
    )
    profile = decode_form_payload(parse_brace_text(text), max_items=20)
    props = {prop["name"]: prop["value"] for props in profile["items"][0]["semantic"]["groups"].values() for prop in props}

    assert props["ИмяКоманды"] == "Form.Command.КомандаПример1"


def test_decode_form_payload_links_marker_34_button_to_command_by_binding_id() -> None:
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{0},0,0,'
        '{34,{15,02023637-7868-4a5f-8576-835a76e0c9ba},0,0,1,"ФормаКомандаОбновить",{1,0},1,{2,409b9a53-7f7e-4178-86c1-33176c7c7a7a},{0},3,0,0,0,2,2,0,0,0,{},0},0},'
        '"Процедура КомандаПример1(Команда)\nКонецПроцедуры",'
        '{0,0},'
        '{0,0},'
        '{4,3,{11,{1},0,"КомандаПрименить"},{11,{2},0,"КомандаПример1"},{11,{3},0,"КомандаПример2"}},'
        '{0,0},{0,0},0,0}'
    )
    profile = decode_form_payload(parse_brace_text(text), max_items=20)
    props = {prop["name"]: prop["value"] for props in profile["items"][0]["semantic"]["groups"].values() for prop in props}

    assert profile["items"][0]["command_binding"]["command_id"] == "2"
    assert props["ИмяКоманды"] == "Form.Command.КомандаПример1"
    assert profile["button_command_links"][0]["match_by"] == "form_command_id"


def test_decode_form_payload_links_marker_34_button_to_standard_command_by_binding_guid() -> None:
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{0},0,0,'
        '{34,{57,02023637-7868-4a5f-8576-835a76e0c9ba},0,0,0,"ТЗИзменитьФорму",{1,0},1,{0,198ea630-fda2-4cda-8a23-f999f4c67ee6},{0},3,0,0,0,2,2,0,0,0,{},0},0},'
        '"",'
        '{0,0},'
        '{0,0},'
        '{0,0},'
        '{0,0},{0,0},0,0}'
    )
    profile = decode_form_payload(parse_brace_text(text), max_items=20)
    props = {prop["name"]: prop["value"] for props in profile["items"][0]["semantic"]["groups"].values() for prop in props}

    assert profile["items"][0]["command_binding"]["scope"] == "standard"
    assert props["ИмяКоманды"] == "Form.StandardCommand.CustomizeForm"
    assert profile["button_command_links"][0]["match_by"] == "standard_command_guid"


def test_decode_form_payload_reads_marker_48_and_73_names_from_xml_verified_slots() -> None:
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{0},0,0,'
        '{48,{3,02023637-7868-4a5f-8576-835a76e0c9ba},0,0,0,2,"А",1,0,"","","","",1,0},'
        '{73,{20,02023637-7868-4a5f-8576-835a76e0c9ba},0,0,0,"ТЗ",0,0,1,"","",{},0,1,0},'
        '0},'
        '"",'
        '{0,0},'
        '{0,0},'
        '{0,0},'
        '{0,0},{0,0},0,0}'
    )
    profile = decode_form_payload(parse_brace_text(text), max_items=20, include_parameters=True)
    by_id = {item["id"]: item for item in profile["items"]}

    assert by_id["3"]["name"] == "А"
    assert by_id["3"]["type_code"] == "2"
    assert by_id["3"]["type_name"] == "Поле ввода"
    assert by_id["20"]["name"] == "ТЗ"


def test_decode_form_payload_links_element_events_to_module_routines() -> None:
    text = (
        '{4,'
        '{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
        '{0},0,0,'
        '{37,{40,02023637-7868-4a5f-8576-835a76e0c9ba},0,0,0,2,"ОформленоОрганизации",0,0,{1,1,{"ru","Организации"}},{1,0},{1,{13}},'
        '{2,1960479b-4d89-4eba-8b39-0aa802020558,"ОформленоОрганизацииНачалоВыбора",b50dc41b-c15a-4ebe-a17f-d01e51c47de6,"ОформленоОрганизацииОчистка"}},0},'
        '"Процедура ОформленоОрганизацииНачалоВыбора()\nКонецПроцедуры",'
        '{4,1,{9,{13},0,"ОрганизацииСАТУРНПредставление",{1,1,{"ru","Организации представление"}},{"Pattern",{"S"}},0,0,0,0,{0,0}}},'
        '{0,0},'
        '{0,0},'
        '{0,0},{0,0},0,0}'
    )
    profile = decode_form_payload(parse_brace_text(text), max_items=20)
    field = {item["name"]: item for item in profile["items"]}["ОформленоОрганизации"]

    assert [event["handler"] for event in field["events"]] == ["ОформленоОрганизацииНачалоВыбора", "ОформленоОрганизацииОчистка"]
    assert [event["name"] for event in field["semantic"]["groups"]["События"]] == ["StartChoice", "Clearing"]
    assert field["handler_links"][0]["status"] == "resolved"
    assert field["handler_links"][1]["status"] == "missing"


def test_event_handlers_from_node_does_not_assign_child_events_to_parent_container() -> None:
    page = parse_brace_text('{22,{},0,0,0,4,"Page1",{},'
        '{37,{40,02023637-7868-4a5f-8576-835a76e0c9ba},0,0,0,2,"List1",0,0,{1,1,{"ru","List1"}},{1,0},{1,{13}},'
        '{1,60edb81d-887b-478e-94ee-7fef2b13393d,"List1OnActivate"}},0}'
        '}'
    )["items"][0]
    child = page["items"][8]
    page_events = event_handlers_from_node(page, "1.27.31", owner="Page1")
    child_events = event_handlers_from_node(child, "1.27.31.8", owner="List1")

    assert page_events == []
    assert len(child_events) == 1
    assert child_events[0]["owner"] == "List1"
    assert child_events[0]["guid"] == "60edb81d-887b-478e-94ee-7fef2b13393d"
    assert child_events[0]["handler"] == "List1OnActivate"


def test_event_handlers_ignores_numeric_non_bsl_handlers(monkeypatch: pytest.MonkeyPatch) -> None:
    node = parse_brace_text(
        '{3,60edb81d-887b-478e-94ee-7fef2b13393d,"HandlerOpen",12345678-1234-1234-1234-1234567890ab,2,87654321-4321-4321-4321-ba0987654321,3}'
    )
    monkeypatch.setattr("parser.form_payload.get_by_path", lambda tree, path: node)

    events = event_handlers({})

    assert len(events) == 1
    assert events[0]["handler"] == "HandlerOpen"


def test_event_handlers_discovers_compact_base_form_event_block() -> None:
    form_values = ["0"] * 24
    form_values[23] = (
        '{2,390d5e4b-e732-4c88-8748-9e211a416984,"ПриЧтенииНаСервере",'
        'bf0ac0e1-bcbb-4dfe-8fc4-0b1923b461a6,"ПередЗаписьюНаСервере",1,0}'
    )
    tree = parse_brace_text("{4,{" + ",".join(form_values) + "}}")

    events = event_handlers(tree)

    assert [(event["name"], event["handler"]) for event in events] == [
        ("OnReadAtServer", "ПриЧтенииНаСервере"),
        ("BeforeWriteAtServer", "ПередЗаписьюНаСервере"),
    ]


def test_public_metadata_item_hides_reference_type_guid() -> None:
    record = {
        "likely_name": "Ответственный",
        "type": {
            "code": "#",
            "kind": "reference",
            "presentation": "Ссылка",
            "type_guid": "a0c73d8d-dbc6-4ea9-ba05-d0ea3681cda2",
        },
        "identity": {
            "guid": "c3a0c22f-d25c-484e-bca4-1914f9e5a661",
            "name": "Ответственный",
            "synonyms": {"ru": "Ответственный"},
        },
        "path": "5.56",
        "index": 54,
        "guids_sample": ["a0c73d8d-dbc6-4ea9-ba05-d0ea3681cda2"],
    }
    resolved_types = {
        "a0c73d8d-dbc6-4ea9-ba05-d0ea3681cda2": {
            "status": "ok",
            "guid_role": "generated_type",
            "generated_category": "Ref",
            "generated_name": "CatalogRef.Пользователи",
            "kind": "Catalog",
            "name": "Пользователи",
            "presentation": "cfg:CatalogRef.Пользователи",
        }
    }

    public = public_metadata_item(record, resolved_types)

    assert public["type"]["presentation"] == "СправочникСсылка.Пользователи"
    assert public["synonym"] == "Ответственный"
    assert "type_guid" not in public["type"]
    assert "resolved" not in public["type"]
    assert "identity" not in public
    assert "guids_sample" not in public


def test_public_semantic_profile_hides_reference_type_guid() -> None:
    semantic = {
        "sections": [
            {
                "category": "Attribute",
                "storage_routes": [{"table": "Config"}],
                "records": [
                    {
                        "likely_name": "Ответственный",
                        "type": {
                            "code": "#",
                            "kind": "reference",
                            "presentation": "Ссылка",
                            "type_guid": "a0c73d8d-dbc6-4ea9-ba05-d0ea3681cda2",
                        },
                        "path": "5.56",
                        "index": 54,
                        "strings_sample": ["Pattern"],
                        "guids_sample": ["a0c73d8d-dbc6-4ea9-ba05-d0ea3681cda2"],
                    }
                ],
            }
        ],
        "section_rules": [{"path": "5", "category": "Attribute"}],
        "generic_sections": [{"path": "1", "strings_sample": ["ПлатежноеПоручение"]}],
    }
    resolved_types = {
        "a0c73d8d-dbc6-4ea9-ba05-d0ea3681cda2": {
            "status": "ok",
            "generated_category": "Ref",
            "kind": "Catalog",
            "name": "Пользователи",
        }
    }

    public = public_semantic_profile(semantic, resolved_types=resolved_types)
    record = public["sections"][0]["records"][0]

    assert record["type"]["presentation"] == "СправочникСсылка.Пользователи"
    assert "type_guid" not in record["type"]
    assert "section_rules" not in public
    assert "generic_sections" not in public
    assert "storage_routes" not in public["sections"][0]
    assert "path" not in record
    assert "guids_sample" not in record


def test_public_form_profile_hides_decoder_coordinates() -> None:
    profile = {
        "schema": "onec_form_payload_profile.v1",
        "status": "ok",
        "items": [
            {
                "name": "ФормаКоманднаяПанель",
                "name_path": "1.34.6",
                "id_path": "1.34.1.0",
                "path": "1.34",
                "marker": "22",
                "type_code": "9",
                "strings_sample": ["Pattern"],
                "guids_sample": ["02023637-7868-4a5f-8576-835a76e0c9ba"],
            }
        ],
        "commands": [
            {
                "name": "Команда1",
                "path": "5.1",
                "id_path": "5.1.1",
                "strings_sample": ["Команда1"],
            }
        ],
    }

    public = public_form_profile(profile)
    item = public["items"][0]
    command = public["commands"][0]

    assert item["name"] == "ФормаКоманднаяПанель"
    assert "name_path" not in item
    assert "id_path" not in item
    assert "path" not in item
    assert "strings_sample" not in item
    assert "guids_sample" not in item
    assert command["name"] == "Команда1"
    assert "path" not in command
    assert "strings_sample" not in command


def test_decode_form_xml_exposes_configurator_semantic_groups() -> None:
    xml = """<?xml version="1.0" encoding="UTF-8"?>
<Form version="2.21">
  <CommandBarLocation>None</CommandBarLocation>
  <ShowCommandBar>false</ShowCommandBar>
  <ChildItems>
    <LabelField name="Номер" id="16">
      <DataPath>Список.Number</DataPath>
      <DefaultItem>true</DefaultItem>
    </LabelField>
    <Button name="СписокАрхивироватьДокументы" id="172">
      <Type>CommandBarButton</Type>
      <CommandName>Form.Command.АрхивироватьДокументы</CommandName>
      <LocationInCommandBar>InAdditionalSubmenu</LocationInCommandBar>
    </Button>
    <InputField name="ОформленоОрганизации" id="40">
      <DataPath>ОрганизацииСАТУРНПредставление</DataPath>
      <TitleLocation>None</TitleLocation>
      <DropListButton>true</DropListButton>
      <ChoiceButton>true</ChoiceButton>
      <ClearButton>true</ClearButton>
      <AutoMaxWidth>false</AutoMaxWidth>
      <MaxWidth>28</MaxWidth>
    </InputField>
    <CheckBoxField name="Пометка" id="41">
      <DataPath>Пометка</DataPath>
    </CheckBoxField>
    <SearchStringAddition name="СтрокаПоиска" id="42">
      <Visible>true</Visible>
    </SearchStringAddition>
  </ChildItems>
  <Commands>
    <Command name="Выполнить" id="1"><Action>Выполнить</Action></Command>
  </Commands>
  <Attributes>
    <Attribute name="Объект" id="2">
      <Columns>
        <AdditionalColumns table="Объект.Строки">
          <Column name="Значение" id="1"/>
        </AdditionalColumns>
      </Columns>
    </Attribute>
  </Attributes>
</Form>
"""
    profile = decode_form_xml(xml)

    assert profile["status"] == "ok"
    assert profile["counts"]["items_total"] == 8
    form_groups = profile["form"]["semantic"]["groups"]
    assert {"name": "ПоложениеКоманднойПанели", "xml_name": "CommandBarLocation", "value": "None", "source": "form_xml", "status": "ok"} in form_groups["Основные"]
    assert {"name": "ОтображатьКоманднуюПанель", "xml_name": "ShowCommandBar", "value": False, "source": "form_xml", "status": "ok"} in form_groups["Основные"]
    by_name = {item["name"]: item for item in profile["items"]}
    number = by_name["Номер"]["semantic"]["groups"]
    assert {"name": "ПутьКДанным", "xml_name": "DataPath", "value": "Список.Number", "source": "form_xml", "status": "ok"} in number["Основные"]
    assert {"name": "АктивизироватьПоУмолчанию", "xml_name": "DefaultItem", "value": True, "source": "form_xml", "status": "ok"} in number["Основные"]

    button = by_name["СписокАрхивироватьДокументы"]["semantic"]["groups"]
    assert {"name": "ИмяКоманды", "xml_name": "CommandName", "value": "Form.Command.АрхивироватьДокументы", "source": "form_xml", "status": "ok"} in button["Основные"]
    assert {"name": "ПоложениеВКоманднойПанели", "xml_name": "LocationInCommandBar", "value": "InAdditionalSubmenu", "source": "form_xml", "status": "ok"} in button["Расположение"]

    field = by_name["ОформленоОрганизации"]["semantic"]["groups"]
    assert {"name": "КнопкаВыпадающегоСписка", "xml_name": "DropListButton", "value": True, "source": "form_xml", "status": "ok"} in field["Использование"]
    assert {"name": "МаксимальнаяШирина", "xml_name": "MaxWidth", "value": "28", "source": "form_xml", "status": "ok"} in field["Расположение"]
    assert by_name["Пометка"]["kind_ru"] == "Поле флажка"
    assert by_name["СтрокаПоиска"]["kind_ru"] == "Дополнение строки поиска"
    assert by_name["Выполнить"]["kind_ru"] == "Команда формы"
    assert by_name["Значение"]["additional_columns_table"] == "Объект.Строки"
    command = by_name["Выполнить"]["semantic"]["groups"]
    assert {"name": "Действие", "xml_name": "Action", "value": "Выполнить", "source": "form_xml", "status": "ok"} in command["Использование"]


def test_payload_public_properties_detects_template_tabular_document() -> None:
    public = payload_public_properties(
        {
            "role": "template_payload",
            "markers": ["MOXCEL", "utf8_bom"],
            "root": {"root_marker": "8"},
            "counts": {"stream_blocks": 0, "base64_blocks": 0},
            "stream_blocks": [],
            "base64_blocks": [],
        }
    )

    assert public["content_kind"] == "template"
    assert public["features"]["tabular_document"] is True
    assert public["root_marker"] == "8"


def test_compare_template_sql_xml_profiles_reports_coordinate_hint_progress() -> None:
    from scripts.compare_1c_template_sql_xml_profiles import compare, render_markdown

    payload = compare(
        {
            "base_id": "upo_test",
            "items": [
                {
                    "owner": {"kind": "Document", "name": "АвансовыйОтчет"},
                    "template": {"name": "ПФ_MXL_АвансовыйОтчет"},
                    "profile": {
                        "capacity_dimensions": {"rows": 75, "columns": 26},
                        "used_dimensions": {"rows": 74, "columns": 25},
                        "counts": {"cells": 0, "cell_coordinate_hints": 10, "cell_style_coordinate_hints": 10, "merge_record_block_candidates": 1},
                    },
                }
            ],
        },
        {
            "root": "xml",
            "templates": [
                {
                    "path": r"Z:\codex\1C\XML\UPO\Структура базы 1с\Конфигурация\Documents\АвансовыйОтчет\Templates\ПФ_MXL_АвансовыйОтчет\Ext\Template.xml",
                    "xml_kind": "tabular_document",
                    "capacity_dimensions": {"rows": 75, "columns": 26},
                    "used_dimensions": {"rows": 75, "columns": 26},
                    "counts": {"cells": 100, "parameters": 0, "merges": 12, "format_indexes": 0},
                }
            ],
        },
    )

    comparison = payload["comparisons"][0]
    assert payload["counts"]["progress_counts"] == {"cell_coordinate_hints_available": 1, "merge_record_blocks_available": 1}
    assert comparison["progress"][0]["sql"] == 10
    assert comparison["progress"][0]["coverage_ratio"] == 0.1
    assert comparison["progress"][1]["sql"] == 1
    assert comparison["progress"][1]["xml_merges"] == 12
    assert comparison["sql"]["used_delta"] == {"rows": -1, "columns": -1}
    assert [gap["code"] for gap in comparison["gaps"]] == ["used_dimensions_mismatch", "cells_missing_or_limited", "merged_ranges_missing"]
    markdown = render_markdown(payload)
    assert "| SQL used | XML used | Used delta | SQL cells | Coord hints | Merge blocks |" in markdown
    assert "`74x25` | `75x26` | `-1x-1` | 0 | 10 | 1" in markdown


def test_extract_moxel_public_structure_decodes_named_area_ranges() -> None:
    payload = (
        "MOXCEL\x00\x08\x00\x01\x00\x0c\x00\ufeff{8,1,12,\r\n"
        '{"ru","ru",1,1,"ru","Русский","Русский",1},\r\n'
        "{128,72},\r\n"
        '0,0,2,0,\r\n'
        '{16,4,{1,1,{"ru","[ДатаПлана] Заголовок"}},0},1,\r\n'
        '{24,5,"Номенклатура",{1,1,{"","НоменклатураПредставление"}},0},2,\r\n'
        '{35,"ОбластьБлюдо",\r\n'
        "{1,\r\n"
        "{3,0,6,1,6,00000000-0000-0000-0000-000000000000},0},"
        '"ОбластьИтог",\r\n'
        "{1,\r\n"
        "{3,0,9,1,16,00000000-0000-0000-0000-000000000000},0}"
        "}"
    ).encode("utf-8")

    structure = adapter_server.extract_moxel_public_structure(payload)

    assert structure["capabilities"]["decoded_text"] is True
    assert structure["capabilities"]["named_area_coordinates"] is True
    assert structure["dimensions"] == {"rows": 128, "columns": 72}
    assert structure["capacity_dimensions"] == {"rows": 128, "columns": 72}
    assert structure["used_dimensions"] == {
        "rows": 2,
        "columns": 17,
        "evidence": ["cells", "cell_coordinate_hints", "named_areas"],
        "bounded_by_capacity": {"rows": True, "columns": True},
    }
    assert structure["counts"]["cells"] == 2
    assert structure["counts"]["cell_parameters"] == 2
    assert structure["counts"]["named_areas"] == 2
    assert structure["capabilities"]["cell_coordinates"] is True
    assert structure["cells"][0]["one_based"] == {"row": 1, "column": 2}
    assert structure["cells"][0]["text"] == "[ДатаПлана] Заголовок"
    assert structure["cells"][1]["one_based"] == {"row": 1, "column": 3}
    assert structure["cells"][1]["parameter"] == "Номенклатура"
    assert structure["cells"][1]["text"] == "НоменклатураПредставление"
    assert {"name": "ДатаПлана", "row": 1, "column": 2, "source": "placeholder", "cell_text": "[ДатаПлана] Заголовок", "one_based": {"row": 1, "column": 2}} in structure["cell_parameters"]
    assert {"name": "Номенклатура", "row": 1, "column": 3, "source": "cell_parameter", "cell_text": "НоменклатураПредставление", "one_based": {"row": 1, "column": 3}} in structure["cell_parameters"]
    assert {"name": "НоменклатураПредставление", "row": 1, "column": 3, "source": "cell_text_identifier", "parameter": "Номенклатура", "one_based": {"row": 1, "column": 3}} in structure["cell_text_identifiers"]
    assert structure["named_areas"][0]["name"] == "ОбластьБлюдо"
    assert structure["named_areas"][0]["range"]["zero_based"] == {
        "top": 0,
        "left": 6,
        "bottom": 1,
        "right": 6,
        "row_start": 0,
        "column_start": 6,
        "row_end": 1,
        "column_end": 6,
    }
    assert structure["named_areas"][1]["range"]["width"] == 8
    assert structure["capabilities"]["merged_cell_candidates"] is True
    assert structure["merged_range_candidates"][0]["source"] == "heuristic_named_area_range"
    assert structure["merged_range_candidates"][0]["confidence"] == "low"
    assert structure["moxel_record_diagnostics"]["schema"] == "moxel_record_diagnostics.v1"
    assert structure["moxel_record_diagnostics"]["authoritative_merge_decoder"] is False
    assert structure["moxel_record_diagnostics"]["head_counts"]
    assert structure["moxel_record_diagnostics"]["head_samples"]
    assert structure["moxel_record_diagnostics"]["head_samples"][0]["samples"]
    assert "tree_position" in structure["moxel_record_diagnostics"]["head_samples"][0]["samples"][0]
    assert structure["moxel_record_diagnostics"]["top_level_records"]
    assert structure["moxel_record_diagnostics"]["top_level_records"][0]["tree_position"] == "$.4"
    assert structure["moxel_record_diagnostics"]["top_level_shapes"]
    assert {"head", "list_length", "numeric_count", "count", "positions", "numeric_prefixes"} <= set(structure["moxel_record_diagnostics"]["top_level_shapes"][0])
    assert structure["moxel_record_diagnostics"]["top_level_shape_candidates"]
    assert {"rank", "score", "reasons", "confidence", "source", "suggested_windows"} <= set(structure["moxel_record_diagnostics"]["top_level_shape_candidates"][0])
    assert structure["moxel_record_diagnostics"]["top_level_shape_candidates"][0]["rank"] == 1
    assert structure["moxel_record_diagnostics"]["top_level_candidate_summary"]["total"] >= 1
    assert structure["moxel_record_diagnostics"]["top_level_candidate_summary"]["score_counts"]
    assert structure["moxel_record_diagnostics"]["top_level_candidate_summary"]["reason_counts"]
    assert structure["moxel_record_diagnostics"]["samples"]


def test_moxel_dimensions_split_used_from_format_extents() -> None:
    capacity_dimensions = {"rows": 128, "columns": 72}
    used_dimensions = adapter_server.infer_moxel_used_dimensions(
        capacity_dimensions=capacity_dimensions,
        cells=[{"row": 2, "column": 3, "text": "Итого"}],
        named_areas=[],
        named_range_candidates=[],
        merged_ranges=[],
    )
    format_dimensions = adapter_server.infer_moxel_format_dimensions(
        capacity_dimensions=capacity_dimensions,
        column_widths=[{"column": 12, "width": 1000}],
        row_heights=[],
    )

    assert used_dimensions == {
        "rows": 2,
        "columns": 3,
        "evidence": ["cells"],
        "bounded_by_capacity": {"rows": True, "columns": True},
    }
    assert format_dimensions == {
        "rows": None,
        "columns": 12,
        "evidence": ["column_widths"],
        "bounded_by_capacity": {"rows": True, "columns": True},
    }


def test_moxel_used_dimensions_prefers_coordinate_hint_columns() -> None:
    used_dimensions = adapter_server.infer_moxel_used_dimensions(
        capacity_dimensions={"rows": 128, "columns": 72},
        cells=[{"row": 74, "column": 72, "text": "bad decoded column"}],
        cell_coordinate_hints=[
            {
                "text": "bad decoded column",
                "one_based": {"row": 74, "column": 26},
                "confidence": "high",
            }
        ],
        named_areas=[],
        named_range_candidates=[],
        merged_ranges=[],
    )

    assert used_dimensions == {
        "rows": 74,
        "columns": 26,
        "evidence": ["cells", "cell_coordinate_hints"],
        "bounded_by_capacity": {"rows": True, "columns": True},
    }


def test_extract_moxel_merge_record_block_candidates_from_singleton_count() -> None:
    diagnostics = {
        "top_level_records": [
            {"tree_position": "$.10", "numeric_items": [3]},
            {"tree_position": "$.11", "numeric_items": [1065758, 0, 0, 0, 0, 64, 768, 3, 1]},
            {"tree_position": "$.12", "numeric_items": [1049374, 1, 1, 1, 1, 32, 24, 1]},
            {"tree_position": "$.13", "numeric_items": [1098526, 1, 1, 1, 1, 6, 24, 3, 1, 1]},
        ]
    }

    candidates = adapter_server.extract_moxel_merge_record_block_candidates(diagnostics)

    assert len(candidates) == 1
    candidate = candidates[0]
    assert candidate["count"] == 3
    assert candidate["tree_position"] == "$.10"
    assert candidate["record_window"] == {"start": "$.11", "end": "$.13", "inspected": 3}
    assert candidate["source"] == "moxel_top_level_count_before_coordinate_block"
    assert candidate["confidence"] == "medium"
    evidence = candidate["evidence"]
    assert evidence["singleton_count_record"] == [3]
    assert evidence["following_long_numeric_records"] == 3
    assert evidence["following_coordinate_like_records"] == 3
    assert evidence["shape_summary"] == [
        {"shape": "1049374:8", "count": 1},
        {"shape": "1065758:9", "count": 1},
        {"shape": "1098526:10", "count": 1},
    ]
    assert evidence["column_edge_hints"] == [
        {"column_or_edge": 1, "source": "moxel_merge_block_scalar_div32", "confidence": "low"},
        {"column_or_edge": 2, "source": "moxel_merge_block_scalar_div32", "confidence": "medium"},
        {"column_or_edge": 24, "source": "moxel_merge_block_scalar_div32", "confidence": "medium"},
    ]
    assert evidence["row_or_size_hints"][:2] == [
        {"value": 3, "count": 2, "positions": ["$.11", "$.13"], "source": "moxel_merge_block_small_scalar", "confidence": "low"},
        {"value": 24, "count": 2, "positions": ["$.12", "$.13"], "source": "moxel_merge_block_small_scalar", "confidence": "low"},
    ]
    assert evidence["record_analysis"]["schema"] == "moxel_numeric_block_records.v1"
    assert evidence["record_analysis"]["records_analyzed"] == 3
    assert evidence["record_analysis"]["packed_div32_by_value"][:3] == [
        {"value": 1, "count": 1, "record_indexes": [2], "runs": [{"start": 2, "end": 2, "length": 1}]},
        {"value": 2, "count": 1, "record_indexes": [1], "runs": [{"start": 1, "end": 1, "length": 1}]},
        {"value": 24, "count": 1, "record_indexes": [1], "runs": [{"start": 1, "end": 1, "length": 1}]},
    ]
    assert evidence["record_analysis"]["records"][0] == {
        "record_index": 1,
        "tree_position": "$.11",
        "shape": "1065758:9",
        "head": 1065758,
        "numeric_count": 9,
        "packed_div32_values": [2, 24],
        "small_scalars": [3, 64],
    }
    assert candidate["sample_records"][0] == {
        "record_index": 1,
        "tree_position": "$.11",
        "numeric_items": [1065758, 0, 0, 0, 0, 64, 768, 3, 1],
        "shape": "1065758:9",
        "packed_div32_values": [2, 24],
        "small_scalars": [64, 3],
    }


def test_moxel_merge_count_hint_does_not_treat_named_area_block_as_merge_records() -> None:
    diagnostics = {
        "top_level_records": [
            {
                "tree_position": "$.346",
                "numeric_items": [24, 0, 13, 0, 1, 1, 2, 2, 3, 3, 4, 4, 6, 5, 7, 6, 5, 7, 8, 8, 9, 9, 10, 15],
            },
            {"tree_position": "$.356", "numeric_items": [1]},
            {"tree_position": "$.357", "numeric_items": [0]},
            {"tree_position": "$.358", "numeric_items": [0]},
            {"tree_position": "$.359", "numeric_items": [3]},
            {"tree_position": "$.360", "numeric_items": [1065758, 0, 0, 0, 0, 64, 768, 3, 1], "strings": ["AllColumns"]},
            {"tree_position": "$.361", "numeric_items": [1049374, 1, 1, 1, 1, 32, 24, 1], "strings": ["AllRows"]},
            {"tree_position": "$.362", "numeric_items": [1098526, 1, 1, 1, 1, 6, 24, 3, 1, 1], "strings": ["Area_Merge_1"]},
        ]
    }

    assert adapter_server.extract_moxel_merge_record_block_candidates(diagnostics) == []

    hints = adapter_server.extract_moxel_merge_count_hints(diagnostics)
    assert len(hints) == 1
    assert hints[0]["count"] == 1
    assert hints[0]["tree_position"] == "$.356"
    assert hints[0]["source"] == "moxel_top_level_merge_count_hint"
    assert hints[0]["evidence"]["following_named_item_count"] == 3
    assert [item["tree_position"] for item in hints[0]["evidence"]["zero_followers"]] == ["$.357", "$.358"]


def test_moxel_merge_record_block_candidates_are_filtered_by_merge_count_hints() -> None:
    candidates = [{"count": 3, "tree_position": "$.359"}, {"count": 1, "tree_position": "$.400"}]

    filtered = adapter_server.filter_moxel_merge_record_block_candidates_by_count_hints(
        candidates,
        [{"count": 1, "tree_position": "$.356"}],
    )

    assert filtered == [{"count": 1, "tree_position": "$.400"}]
    assert adapter_server.filter_moxel_merge_record_block_candidates_by_count_hints(candidates, [{"count": 0}]) == []


def test_extract_moxel_merged_ranges_from_tree_decodes_exclusive_edges() -> None:
    tree = {
        "type": "list",
        "items": [
            {"type": "list", "items": [{"type": "atom", "value": "0"}]},
            {
                "type": "list",
                "items": [
                    {"type": "atom", "value": "4"},
                    {"type": "list", "items": [{"type": "atom", "value": value} for value in ["1", "2", "4", "2", "0"]]},
                    {"type": "list", "items": [{"type": "atom", "value": value} for value in ["7", "3", "7", "6", "0"]]},
                    {"type": "list", "items": [{"type": "atom", "value": value} for value in ["2", "7", "6", "9", "0"]]},
                    {"type": "list", "items": [{"type": "atom", "value": value} for value in ["17", "10", "22", "10", "0"]]},
                ],
            },
        ],
    }

    ranges = adapter_server.extract_moxel_merged_ranges_from_tree(tree, [{"count": 4, "tree_position": "$.1"}])

    assert [item["range"]["zero_based"] for item in ranges] == [
        {"top": 2, "left": 1, "bottom": 2, "right": 3, "row_start": 2, "column_start": 1, "row_end": 2, "column_end": 3},
        {"top": 3, "left": 7, "bottom": 5, "right": 7, "row_start": 3, "column_start": 7, "row_end": 5, "column_end": 7},
        {"top": 7, "left": 2, "bottom": 8, "right": 5, "row_start": 7, "column_start": 2, "row_end": 8, "column_end": 5},
        {"top": 10, "left": 17, "bottom": 10, "right": 21, "row_start": 10, "column_start": 17, "row_end": 10, "column_end": 21},
    ]
    assert ranges[0]["raw"] == {"left": 1, "top": 2, "right_exclusive": 4, "bottom_exclusive": 2, "flag": 0}
    assert ranges[0]["source"] == "moxel_tree_merge_block"


def test_extract_moxel_format_table_from_tree_prefers_contiguous_format_run() -> None:
    def record(values: list[str]) -> dict[str, Any]:
        return {"type": "list", "items": [{"type": "atom", "value": value} for value in values]}

    tree = {
        "type": "list",
        "items": [
            record(["17281", "0", "80", "0", "8", "0"]),
            {"type": "atom", "value": "separator"},
            record(["17281", "0", "80", "0", "8", "0"]),
            record(["17281", "1", "120", "6", "24", "2"]),
            record(["129", "0", "240"]),
        ],
    }

    formats = adapter_server.extract_moxel_format_table_from_tree(tree)

    assert [item["format_index"] for item in formats] == [1, 2, 3]
    assert formats[0]["font_index"] == 0
    assert formats[0]["width"] == 80
    assert formats[0]["horizontal_alignment"] == {"code": 0, "value": "Left"}
    assert formats[0]["vertical_alignment"] == {"code": 8, "value": "Bottom"}
    assert formats[0]["text_placement"] == {"code": 0, "value": "Auto"}
    assert formats[1]["horizontal_alignment"] == {"code": 6, "value": "Center"}
    assert formats[1]["vertical_alignment"] == {"code": 24, "value": "Center"}
    assert formats[1]["text_placement"] == {"code": 2, "value": "Block"}
    assert formats[2]["record_type"] == 129
    assert formats[2]["width"] == 240


def test_extract_moxel_format_table_from_diagnostics_prefers_contiguous_format_run() -> None:
    diagnostics = {
        "top_level_records": [
            {"tree_position": "$.4", "numeric_items": [17281, 0, 80, 0, 8, 0]},
            {"tree_position": "$.274", "numeric_items": [17281, 0, 80, 0, 8, 0]},
            {"tree_position": "$.275", "numeric_items": [17281, 1, 120, 6, 24, 2]},
            {"tree_position": "$.276", "numeric_items": [129, 0, 240]},
        ]
    }

    formats = adapter_server.extract_moxel_format_table_from_diagnostics(diagnostics)

    assert [item["format_index"] for item in formats] == [1, 2, 3]
    assert formats[0]["tree_position"] == "$.274"
    assert formats[1]["horizontal_alignment"] == {"code": 6, "value": "Center"}
    assert formats[1]["text_placement"] == {"code": 2, "value": "Block"}
    assert formats[2]["record_type"] == 129


def test_extract_moxel_format_table_decodes_extended_color_border_flags() -> None:
    diagnostics = {
        "top_level_records": [
            {"tree_position": "$.66", "numeric_items": [1153, 0, 100, 0]},
            {"tree_position": "$.67", "numeric_items": [34945, 0, 120, 3, 2]},
            {"tree_position": "$.68", "numeric_items": [191, 0, 0, 0, 0, 0, 4, 140]},
            {"tree_position": "$.69", "numeric_items": [36031, 0, 1, 0, 2, 0, 4, 160, 5, 6, 1]},
        ]
    }

    formats = adapter_server.extract_moxel_format_table_from_diagnostics(diagnostics)

    assert [item["width"] for item in formats] == [100, 120, 140, 160]
    assert formats[0]["record_type_hex"] == "0x481"
    assert formats[0]["text_color"] == {"style_index": 0, "source": "moxel_format_record_flag_0x0400"}
    assert formats[1]["back_color"] == {"style_index": 3, "source": "moxel_format_record_flag_0x0800"}
    assert formats[1]["fill_type"] == {"code": 2, "value": "Template", "source": "moxel_format_record_flag_0x8000"}
    assert formats[2]["borders"] == {
        "left": 0,
        "top": 0,
        "right": 0,
        "bottom": 0,
        "color_style_index": 4,
        "flags": ["0x0002", "0x0004", "0x0008", "0x0010", "0x0020"],
        "source": "moxel_format_record_border_flags",
    }
    assert formats[3]["borders"]["left"] == 1
    assert formats[3]["borders"]["right"] == 2
    assert formats[3]["text_color"]["style_index"] == 5
    assert formats[3]["back_color"]["style_index"] == 6
    assert formats[3]["fill_type"]["value"] == "Parameter"


def test_extract_moxel_format_table_decodes_partial_border_flags() -> None:
    diagnostics = {
        "top_level_records": [
            {"tree_position": "$.55", "numeric_items": [163, 0, 1, 0, 141]},
            {"tree_position": "$.56", "numeric_items": [165, 0, 1, 0, 142]},
            {"tree_position": "$.57", "numeric_items": [169, 0, 1, 0, 143]},
            {"tree_position": "$.58", "numeric_items": [177, 0, 1, 0, 144]},
        ]
    }

    formats = adapter_server.extract_moxel_format_table_from_diagnostics(diagnostics)

    assert [item["width"] for item in formats] == [141, 142, 143, 144]
    assert formats[0]["borders"] == {
        "left": 1,
        "color_style_index": 0,
        "flags": ["0x0002", "0x0020"],
        "source": "moxel_format_record_border_flags",
    }
    assert formats[1]["borders"]["top"] == 1
    assert formats[2]["borders"]["right"] == 1
    assert formats[3]["borders"]["bottom"] == 1


def test_extract_moxel_format_style_index_table_reports_references_and_candidates() -> None:
    diagnostics = {
        "top_level_records": [
            {"tree_position": "$.55", "numeric_items": [129, 0, 111]},
            {"tree_position": "$.56", "numeric_items": [2177, 0, 112, 2]},
            {"tree_position": "$.57", "numeric_items": [2177, 0, 113, 3]},
            {
                "tree_position": "$.75",
                "numeric_items": [2, 2, 3],
                "child_records": [
                    {
                        "tree_position": "$.75.2",
                        "numeric_items": [4, 3, 3],
                        "child_records": [{"tree_position": "$.75.2.2", "numeric_items": [-25]}],
                    },
                    {
                        "tree_position": "$.75.4",
                        "numeric_items": [4, 3, 3],
                        "child_records": [{"tree_position": "$.75.4.2", "numeric_items": [-26]}],
                    },
                ],
            },
            {
                "tree_position": "$.74",
                "numeric_items": [4, 3, 3],
                "child_records": [{"tree_position": "$.74.2", "numeric_items": [-1]}],
            },
            {
                "tree_position": "$.76",
                "numeric_items": [4, 0, 0],
                "child_records": [{"tree_position": "$.76.2", "numeric_items": [12971252]}],
            },
        ]
    }
    formats = adapter_server.extract_moxel_format_table_from_diagnostics(diagnostics)

    table = adapter_server.extract_moxel_format_style_index_table(formats, diagnostics)

    assert table["counts"] == {"style_references": 2, "candidate_records": 1, "style_object_candidates": 4}
    assert [(item["style_index"], item["roles"], item["format_indexes"]) for item in table["style_references"]] == [
        (2, ["back_color"], [2]),
        (3, ["back_color"], [3]),
    ]
    assert table["style_references"][0]["style_code_candidates"][0]["style_code"] == -25
    assert table["style_references"][0]["style_code"] == -25
    assert table["style_references"][1]["style_code_candidates"][0]["style_code"] == -26
    assert table["style_references"][1]["style_code"] == -26
    assert table["candidate_records"][0]["tree_position"] == "$.75"
    assert table["candidate_records"][0]["referenced_style_indexes"] == [2, 3]
    assert table["style_object_candidates"][0]["style_code"] == -25
    assert table["style_references"][0]["style_object"]["style_code"] == -1
    assert table["style_references"][1]["style_object"]["color"]["hex"] == "0xC5ECF4"
    assert table["style_object_candidates"][3]["color"]["rgb_little_endian"]["hex"] == "#F4ECC5"


def test_enrich_moxel_format_table_with_style_references_copies_to_cell_links() -> None:
    formats = [
        {
            "format_index": 2,
            "font_index": 0,
            "width": 112,
            "back_color": {"style_index": 2, "source": "moxel_format_record_flag_0x0800"},
            "borders": {"left": 1, "color_style_index": 3},
        }
    ]
    style_table = {
        "style_references": [
            {
                "style_index": 2,
                "roles": ["back_color"],
                "style_object": {"kind": "packed_color", "color": {"hex": "0xC5ECF4"}},
                "source": "moxel_format_record_style_index",
                "confidence": "medium",
            },
            {
                "style_index": 3,
                "roles": ["border_color"],
                "style_code": -28,
                "source": "moxel_format_record_style_index",
                "confidence": "medium",
            },
        ]
    }

    enriched = adapter_server.enrich_moxel_format_table_with_style_references(formats, style_table)
    links = adapter_server.extract_moxel_cell_format_links(
        [{"row": 1, "column": 1, "cell_id": 2, "text": "A"}],
        enriched,
    )

    assert enriched[0]["back_color"]["style_reference"]["style_object"]["color"]["hex"] == "0xC5ECF4"
    assert enriched[0]["back_color"]["resolved"]["color"]["hex"] == "0xC5ECF4"
    assert enriched[0]["back_color"]["resolved_style"]["color"]["hex"] == "0xC5ECF4"
    assert enriched[0]["borders"]["color_style_reference"]["style_code"] == -28
    assert enriched[0]["borders"]["color_resolved"]["style_code"] == -28
    assert links[0]["format"]["back_color"]["style_reference"]["style_object"]["kind"] == "packed_color"
    assert links[0]["format"]["back_color"]["resolved"]["kind"] == "packed_color"
    assert links[0]["format"]["borders"]["color_style_reference"]["style_code"] == -28
    assert links[0]["format"]["borders"]["color_resolved"]["kind"] == "style_code"

    fallback = adapter_server.enrich_moxel_format_table_with_style_references(
        [
            {
                "format_index": 2,
                "back_color": {
                    "style_index": 2,
                    "style_reference": style_table["style_references"][0],
                },
            }
        ],
        {},
    )
    assert fallback[0]["back_color"]["resolved"]["color"]["hex"] == "0xC5ECF4"


def test_extract_moxel_record_diagnostics_includes_top_level_child_records() -> None:
    tree = {
        "type": "list",
        "items": [
            {
                "type": "list",
                "items": [
                    {"type": "atom", "value": "2"},
                    {
                        "type": "list",
                        "items": [
                            {"type": "atom", "value": "2"},
                            {"type": "string", "value": "A"},
                            {
                                "type": "list",
                                "items": [{"type": "atom", "value": "9"}, {"type": "string", "value": "AA"}],
                            },
                        ],
                    },
                    {
                        "type": "list",
                        "items": [{"type": "atom", "value": "3"}, {"type": "string", "value": "B"}],
                    },
                ],
            }
        ],
    }

    diagnostics = adapter_server.extract_moxel_record_diagnostics(tree)

    record = diagnostics["top_level_records"][0]
    assert record["numeric_items"] == [2]
    assert record["child_records"] == [
        {
            "tree_position": "$.0.1",
            "head": 2,
            "list_length": 3,
            "numeric_items": [2],
            "numeric_items_truncated": False,
            "strings": ["A"],
            "strings_truncated": False,
            "child_records": [
                {
                    "tree_position": "$.0.1.2",
                    "head": 9,
                    "list_length": 2,
                    "numeric_items": [9],
                    "numeric_items_truncated": False,
                    "strings": ["AA"],
                    "strings_truncated": False,
                }
            ],
        },
        {
            "tree_position": "$.0.2",
            "head": 3,
            "list_length": 2,
            "numeric_items": [3],
            "numeric_items_truncated": False,
            "strings": ["B"],
            "strings_truncated": False,
        },
    ]


def test_extract_moxel_font_table_from_diagnostics_decodes_face_height_and_weight() -> None:
    diagnostics = {
        "top_level_records": [
            {
                "tree_position": "$.285",
                "numeric_items": [8, 0, 575, 80, 0, 0, 0, 400, 0, 0, 0, 0, 0, 0, 0, 0, 1, 100, 0],
                "strings": ["Arial"],
            },
            {
                "tree_position": "$.286",
                "numeric_items": [8, 0, 575, 100, 0, 0, 0, 700, 0, 0, 0, 0, 0, 0, 0, 0, 1, 100, 0],
                "strings": ["Courier New"],
            },
        ]
    }

    fonts = adapter_server.extract_moxel_font_table_from_diagnostics(diagnostics)

    assert fonts[0]["font_index"] == 0
    assert fonts[0]["face_name"] == "Arial"
    assert fonts[0]["height"] == 8.0
    assert fonts[0]["bold"] is False
    assert fonts[1]["font_index"] == 1
    assert fonts[1]["face_name"] == "Courier New"
    assert fonts[1]["height"] == 10.0
    assert fonts[1]["bold"] is True


def test_extract_moxel_font_table_decodes_style_bits() -> None:
    diagnostics = {
        "top_level_records": [
            {
                "tree_position": "$.62",
                "numeric_items": [8, 0, 575, 90, 0, 0, 0, 400, 1, 0, 0, 0, 0, 0, 0, 0, 1, 100, 0],
                "strings": ["Courier New"],
            },
            {
                "tree_position": "$.63",
                "numeric_items": [8, 0, 575, 80, 0, 0, 0, 400, 0, 1, 1, 0, 0, 0, 0, 0, 1, 100, 0],
                "strings": ["Arial"],
            },
        ]
    }

    fonts = adapter_server.extract_moxel_font_table_from_diagnostics(diagnostics)

    assert fonts[0]["italic"] is True
    assert fonts[0]["underline"] is False
    assert fonts[1]["underline"] is True
    assert fonts[1]["strikeout"] is True


def test_enrich_moxel_format_table_with_fonts_keeps_font_index_zero() -> None:
    enriched = adapter_server.enrich_moxel_format_table_with_fonts(
        [{"format_index": 1, "font_index": 0, "width": 80}, {"format_index": 2, "font_index": 1, "width": 120}],
        [
            {"font_index": 0, "face_name": "Arial", "height": 8.0, "weight": 400, "bold": False},
            {"font_index": 1, "face_name": "Courier New", "height": 10.0, "weight": 700, "bold": True},
        ],
    )

    assert enriched[0]["font"]["face_name"] == "Arial"
    assert enriched[1]["font"]["bold"] is True


def test_extract_moxel_cell_format_links_maps_cell_id_to_format_index() -> None:
    links = adapter_server.extract_moxel_cell_format_links(
        [
            {"row": 1, "column": 2, "cell_id": 3, "text": "FMT_R1C2"},
            {"row": 1, "column": 3, "cell_id": 99, "text": "no format"},
        ],
        [
            {
                "format_index": 3,
                "font_index": 0,
                "width": 160,
                "font": {"font_index": 0, "face_name": "Arial", "height": 8.0, "bold": False},
                "horizontal_alignment": {"code": 2, "value": "Right"},
                "text_placement": {"code": 1, "value": "Cut"},
            },
        ],
    )

    assert links == [
        {
            "row": 1,
            "column": 2,
            "one_based": {"row": 1, "column": 2},
            "zero_based": {"row": 0, "column": 1},
            "format_index": 3,
            "format": {
                "format_index": 3,
                "font_index": 0,
                "width": 160,
                "font": {"font_index": 0, "face_name": "Arial", "height": 8.0, "bold": False},
                "horizontal_alignment": {"code": 2, "value": "Right"},
                "text_placement": {"code": 1, "value": "Cut"},
            },
            "text": "FMT_R1C2",
            "source": "moxel_cell_id_as_format_index",
            "confidence": "medium",
            "diagnostics": {
                "message": "In controlled MOXCEL fixtures this cell scalar matches XML <f>/formatIndex. Validate on more one-property probes before treating it as an authoritative style binding."
            },
        }
    ]


def test_summarize_moxel_cell_format_links_reports_coverage_confidence() -> None:
    high = adapter_server.summarize_moxel_cell_format_links(
        [{"row": row, "column": column} for row in range(1, 3) for column in range(1, 4)],
        [{"format_index": 1}, {"format_index": 2}, {"format_index": 3}],
        [
            {"row": row, "column": column, "format_index": column}
            for row in range(1, 3)
            for column in range(1, 4)
        ],
    )
    low = adapter_server.summarize_moxel_cell_format_links(
        [{"row": 1, "column": column} for column in range(1, 101)],
        [{"format_index": 1}],
        [{"row": 1, "column": 1, "format_index": 1}],
    )

    assert high["linked_cells_ratio_percent"] == 100.0
    assert high["distinct_format_indexes"] == 3
    assert high["confidence"] == "high"
    assert low["linked_cells_ratio_percent"] == 1.0
    assert low["confidence"] == "low"


def test_filter_moxel_merge_count_hints_by_tree_removes_positive_slots_without_records() -> None:
    tree = {
        "type": "list",
        "items": [
            {"type": "list", "items": [{"type": "atom", "value": "8"}]},
            {"type": "list", "items": [{"type": "atom", "value": "0"}]},
            {
                "type": "list",
                "items": [
                    {"type": "atom", "value": "1"},
                    {"type": "list", "items": [{"type": "atom", "value": value} for value in ["1", "2", "4", "2", "0"]]},
                ],
            },
        ],
    }

    filtered = adapter_server.filter_moxel_merge_count_hints_by_tree(
        tree,
        [
            {"count": 8, "tree_position": "$.0"},
            {"count": 0, "tree_position": "$.1"},
            {"count": 1, "tree_position": "$.2", "evidence": {"singleton_count_record": [1]}},
        ],
    )

    assert [item["count"] for item in filtered] == [0, 1]
    assert filtered[1]["evidence"]["merge_record_children"] == 1


def test_filter_moxel_merge_count_hints_by_ranges_removes_positive_slots_without_ranges() -> None:
    hints = [
        {"count": 8, "tree_position": "$.0"},
        {"count": 0, "tree_position": "$.1"},
        {"count": 1, "tree_position": "$.2"},
    ]

    assert adapter_server.filter_moxel_merge_count_hints_by_ranges(hints, []) == [{"count": 0, "tree_position": "$.1"}]
    assert adapter_server.filter_moxel_merge_count_hints_by_ranges(hints, [{"range": {"zero_based": {}}}]) == hints


def test_compact_template_structure_formats_section_includes_widths_heights_and_styles() -> None:
    compact = adapter_server.compact_template_structure(
        {
            "format": "MOXCEL",
            "cell_style_candidates": [{"text": "A1"}],
            "cells": [{"row": 1, "column": 1, "cell_id": 1, "text": "A1"}],
            "column_widths": [{"column": 1, "width": 120}],
            "format_table": [{"format_index": 1, "width": 120, "font_index": 0}],
            "font_table": [{"font_index": 0, "face_name": "Arial", "height": 8.0, "bold": False}],
            "row_heights": [{"row": 1, "height": 18}],
        },
        {},
        view="structure",
        sections={"formats"},
    )

    assert compact["cell_style_candidates"] == [{"text": "A1"}]
    assert compact["column_widths"] == [{"column": 1, "width": 120}]
    assert compact["font_table"] == [{"font_index": 0, "face_name": "Arial", "height": 8.0, "bold": False}]
    assert compact["format_table"][0]["font"]["face_name"] == "Arial"
    assert compact["cell_format_links"][0]["format_index"] == 1
    assert compact["cell_format_links"][0]["format"]["width"] == 120
    assert compact["cell_format_links"][0]["format"]["font"]["face_name"] == "Arial"
    assert compact["cell_format_link_stats"]["cell_format_links"] == 1
    assert compact["row_heights"] == [{"row": 1, "height": 18}]


def test_compact_template_structure_recovers_format_table_from_diagnostics() -> None:
    compact = adapter_server.compact_template_structure(
        {
            "format": "MOXCEL",
            "moxel_record_diagnostics": [
                {
                    "top_level_records": [
                        {"tree_position": "$.274", "numeric_items": [17281, 0, 80, 0, 8, 0]},
                        {"tree_position": "$.275", "numeric_items": [129, 1, 120]},
                    ]
                }
            ],
        },
        {},
        view="structure",
        sections={"formats"},
    )

    assert compact["counts"]["format_table"] == 2
    assert [item["width"] for item in compact["format_table"]] == [80, 120]


def test_compact_template_structure_recovers_style_index_table_from_diagnostics() -> None:
    compact = adapter_server.compact_template_structure(
        {
            "format": "MOXCEL",
            "moxel_record_diagnostics": [
                {
                    "top_level_records": [
                        {"tree_position": "$.55", "numeric_items": [129, 0, 111]},
                        {"tree_position": "$.56", "numeric_items": [2177, 0, 112, 2]},
                        {"tree_position": "$.57", "numeric_items": [2177, 0, 113, 3]},
                        {"tree_position": "$.75", "numeric_items": [2, 2, 3]},
                    ]
                }
            ],
        },
        {},
        view="structure",
        sections={"formats"},
    )

    assert compact["counts"]["format_style_index_table"] == 2
    assert [item["style_index"] for item in compact["format_style_index_table"]["style_references"]] == [2, 3]
    assert compact["format_style_index_table"]["candidate_records"][0]["tree_position"] == "$.75"


def test_parse_template_sections_accepts_formats_aliases() -> None:
    assert adapter_server.parse_template_sections({"sections": "summary,formats,format_table,font_table,fonts,format_style_index_table,style_index_table,style_references,cell_format_links,cell_format_link_stats,format_links,row_heights,heights"}) == {
        "formats",
        "format_table",
        "font_table",
        "fonts",
        "format_style_index_table",
        "style_index_table",
        "style_references",
        "cell_format_links",
        "cell_format_link_stats",
        "format_links",
        "row_heights",
        "heights",
    }


def test_merge_template_structures_recomputes_public_dimensions() -> None:
    merged = adapter_server.merge_template_structures(
        [
            {
                "features": {"tabular_document": True},
                "structure": {
                    "format": "MOXCEL",
                    "capabilities": {"cell_coordinates": True, "column_widths": True},
                    "dimensions": {"rows": 128, "columns": 72},
                    "capacity_dimensions": {"rows": 128, "columns": 72},
                    "cells": [{"row": 2, "column": 3, "text": "A"}],
                    "column_widths": [{"column": 12, "width": 1000}],
                },
            },
            {
                "features": {"tabular_document": True},
                "structure": {
                    "format": "MOXCEL",
                    "capabilities": {"cell_coordinates": True},
                    "dimensions": {"rows": 128, "columns": 72},
                    "capacity_dimensions": {"rows": 128, "columns": 72},
                    "cells": [{"row": 5, "column": 4, "text": "B"}],
                },
            },
        ]
    )

    assert merged["capacity_dimensions"] == {"rows": 128, "columns": 72}
    assert merged["used_dimensions"] == {
        "rows": 5,
        "columns": 4,
        "evidence": ["cells"],
        "bounded_by_capacity": {"rows": True, "columns": True},
    }
    assert merged["format_dimensions"] == {
        "rows": None,
        "columns": 12,
        "evidence": ["column_widths"],
        "bounded_by_capacity": {"rows": True, "columns": True},
    }


def test_extract_moxel_cells_from_tree_finds_nested_row_runs() -> None:
    tree = parse_brace_text(
        '{8,1,12,{128,72},{99,{4,0,1,2,{16,2,{1,1,{"ru","nested text"}},0}}}}'
    )

    cells = adapter_server.extract_moxel_cells_from_tree(tree)

    assert len(cells) == 1
    assert cells[0]["text"] == "nested text"
    assert cells[0]["one_based"] == {"row": 5, "column": 3}


def test_extract_moxel_named_area_candidates_from_tree_without_coordinates() -> None:
    tree = parse_brace_text(
        '{8,1,12,{1,"ПримерГоризонтальнойОбласти",{1,{1,-1,3,-1,4,00000000-0000-0000-0000-000000000000},0}}}'
    )

    areas = adapter_server.extract_moxel_named_area_candidates_from_tree(tree)

    assert areas[0]["name"] == "ПримерГоризонтальнойОбласти"
    assert areas[0]["source"] == "moxel_tree_named_area_candidate"
    assert areas[0]["range"] is None
    assert areas[0]["range_candidate"]["raw_scalars"] == [
        "1",
        "1",
        "-1",
        "3",
        "-1",
        "4",
        "00000000-0000-0000-0000-000000000000",
        "0",
    ]


def test_extract_moxel_named_range_candidates_from_tree_includes_cell_name() -> None:
    tree = parse_brace_text(
        '{8,1,12,{2,"R7C2_TEST",{1,{3,1,6,1,6,00000000-0000-0000-0000-000000000000},0},'
        '"ПримерГоризонтальнойОбласти",{1,{1,-1,3,-1,4,00000000-0000-0000-0000-000000000000},0}}}'
    )

    ranges = adapter_server.extract_moxel_named_range_candidates_from_tree(tree)

    assert ranges[0]["name"] == "R7C2_TEST"
    assert ranges[0]["kind"] == "named_cell_or_range"
    assert ranges[0]["range"]["one_based"] == {
        "top": 7,
        "left": 2,
        "bottom": 7,
        "right": 2,
        "row_start": 7,
        "column_start": 2,
        "row_end": 7,
        "column_end": 2,
    }
    assert ranges[0]["range_candidate"]["coordinate_order"] == "left,top,right,bottom"
    assert ranges[0]["range_candidate"]["raw_scalars"] == [
        "1",
        "3",
        "1",
        "6",
        "1",
        "6",
        "00000000-0000-0000-0000-000000000000",
        "0",
    ]
    assert ranges[1]["name"] == "ПримерГоризонтальнойОбласти"
    assert ranges[1]["kind"] == "named_area"


def test_analyze_template_structure_reports_area_widths_and_intersections() -> None:
    structure = {
        "capabilities": {"named_area_coordinates": True, "cell_coordinates": False},
        "named_areas": [
            {"name": "ОбластьА", "occurrence": 1, "range": adapter_server.moxel_range(0, 0, 0, 2)},
            {"name": "ОбластьБ", "occurrence": 1, "range": adapter_server.moxel_range(0, 1, 0, 3)},
        ],
        "merged_range_candidates": [
            {"name": "ОбластьА", "range": adapter_server.moxel_range(0, 0, 0, 2), "confidence": "low"}
        ],
        "cell_parameters": [{"name": "ДатаПлана", "row": 1, "column": 1, "source": "placeholder"}],
        "cell_text_identifiers": [{"name": "НоменклатураПредставление", "row": 1, "column": 2, "source": "cell_text_identifier"}],
        "cell_style_candidates": [
            {
                "text": "Ячейка 7 - 2",
                "tree_position": "$.30",
                "coordinate_hints": {
                    "one_based": {"column": 2},
                    "zero_based": {"column": 1},
                    "source": "moxel_schema_rule:inline_text_column_from_last_preceding_scalar_plus_one",
                },
            }
        ],
        "cell_coordinate_hints": [
            {
                "text": "Ячейка 7 - 2",
                "one_based": {"row": 7, "column": 2},
                "confidence": "high",
                "source": "moxel_inline_text_coordinate_hint",
            }
        ],
        "area_cell_coverage": [{"name": "ОбластьА", "cell_count": 1, "parameter_count": 1}],
        "parameters": [{"name": "ДатаПлана"}, {"name": "НоменклатураПредставление"}],
    }

    analysis = adapter_server.analyze_template_structure(structure)

    assert analysis["checks"]["area_widths"] == "ok"
    assert analysis["counts"]["area_widths"] == 2
    assert analysis["counts"]["intersections_returned"] == 1
    assert analysis["counts"]["merged_range_candidates"] == 1
    assert analysis["counts"]["cell_parameters"] == 1
    assert analysis["counts"]["cell_text_identifiers"] == 1
    assert analysis["counts"]["cell_coordinate_hints"] == 1
    assert analysis["counts"]["cell_style_coordinate_hints"] == 1
    assert analysis["checks"]["parameters_without_cells"] == "ok"
    assert analysis["checks"]["cell_coordinate_hints"] == "ok"
    assert analysis["checks"]["cell_style_coordinate_hints"] == "ok"
    assert analysis["checks"]["merged_cell_candidates"] == "ok"
    assert analysis["cell_coordinate_hints"][0]["one_based"] == {"row": 7, "column": 2}
    assert analysis["cell_style_coordinate_hints"][0]["coordinate_hints"]["one_based"] == {"column": 2}
    assert analysis["intersections"][0]["range"]["one_based"]["left"] == 2
    assert analysis["issues"][0]["code"] == "cell_coordinates_not_decoded"


def test_template_response_summary_keeps_counts_and_limits_lists() -> None:
    structure = {
        "format": "MOXCEL",
        "capabilities": {"cell_coordinates": True, "named_area_coordinates": True},
        "dimensions": {"rows": 10, "columns": 5},
        "capacity_dimensions": {"rows": 10, "columns": 5},
        "used_dimensions": {"rows": 3, "columns": 1, "evidence": ["cells"], "bounded_by_capacity": {"rows": True, "columns": True}},
        "format_dimensions": {"rows": None, "columns": 1, "evidence": ["column_widths"], "bounded_by_capacity": {"rows": True, "columns": True}},
        "named_areas": [
            {"name": "Область1", "range": adapter_server.moxel_range(0, 0, 0, 0)},
            {"name": "Область2", "range": adapter_server.moxel_range(1, 0, 1, 0)},
        ],
        "cells": [
            {"row": 1, "column": 1, "text": "A"},
            {"row": 2, "column": 1, "text": "B"},
            {"row": 3, "column": 1, "text": "C"},
        ],
        "cell_parameters": [{"name": "ДатаПлана", "row": 1, "column": 1}],
        "cell_coordinate_hints": [
            {"text": "A", "one_based": {"row": 1, "column": 1}, "confidence": "high"},
            {"text": "B", "one_based": {"row": 2, "column": 1}, "confidence": "high"},
        ],
        "cell_style_candidates": [
            {
                "text": "A",
                "tree_position": "$.1",
                "coordinate_hints": {"one_based": {"column": 1}, "source": "rule"},
            }
        ],
        "column_widths": [{"column": 1, "width": 1000}],
        "merged_range_candidates": [{"name": "Область1", "range": adapter_server.moxel_range(0, 0, 1, 0)}],
        "moxel_record_diagnostics": [
            {
                "schema": "moxel_record_diagnostics.v1",
                "head_counts": [{"head": 8, "count": 1}, {"head": 16, "count": 2}],
                "head_samples": [
                    {"head": 8, "count": 1, "samples": [{"tree_position": "$", "head": 8}]},
                    {"head": 16, "count": 2, "samples": [{"tree_position": "$.1", "head": 16}, {"tree_position": "$.2", "head": 16}]},
                ],
                "top_level_records": [
                    {"tree_position": "$.1", "head": 8, "numeric_items": [8, 1]},
                    {"tree_position": "$.2", "head": 16, "numeric_items": [16, 2, 10]},
                    {"tree_position": "$.3", "head": 24, "numeric_items": [24, 3]},
                ],
                "top_level_shapes": [
                    {"head": 16, "list_length": 4, "numeric_count": 3, "string_count": 0, "count": 2, "positions": ["$.2"], "numeric_prefixes": [[16, 1, 2]]},
                    {"head": 8, "list_length": 2, "numeric_count": 1, "string_count": 0, "count": 1, "positions": ["$.1"], "numeric_prefixes": [[8]]},
                ],
                "top_level_shape_candidates": [
                    {
                        "rank": 1,
                        "head": 16,
                        "score": 5,
                        "reasons": ["rare_shape"],
                        "positions": ["$.2"],
                        "confidence": "low",
                        "source": "heuristic_top_level_shape",
                        "suggested_windows": [
                            {
                                "center": 2,
                                "start": 0,
                                "end": 4,
                                "request_hint": {
                                    "moxel_record_start": 0,
                                    "moxel_record_end": 4,
                                    "moxel_record_heads": "16",
                                    "moxel_record_context": 1,
                                },
                            },
                            {
                                "center": 2,
                                "start": 2,
                                "end": 2,
                                "request_hint": {
                                    "moxel_record_start": 2,
                                    "moxel_record_end": 2,
                                    "moxel_record_heads": "16",
                                    "moxel_record_context": 0,
                                },
                            },
                        ],
                    },
                    {
                        "rank": 2,
                        "head": 8,
                        "score": 5,
                        "reasons": ["rare_shape"],
                        "positions": ["$.1"],
                        "confidence": "low",
                        "source": "heuristic_top_level_shape",
                        "suggested_windows": [{"center": 1, "start": 0, "end": 3, "request_hint": {"moxel_record_heads": "8"}}],
                    },
                ],
                "top_level_candidate_summary": {
                    "total": 2,
                    "score_min": 5,
                    "score_max": 5,
                    "score_counts": [{"score": 5, "count": 2}],
                    "reason_counts": [{"reason": "rare_shape", "count": 2}],
                },
                "samples": [{"head": 8}, {"head": 16}],
                "coordinate_like_samples": [{"head": 16}],
            }
        ],
        "strings_sample": ["Строка", "ЕщеСтрока"],
        "payload": {"compression": "raw_deflate", "encoding": "cp1251"},
        "tree_root_summary": {"type": "list", "items_count": 3, "head": "8"},
        "moxel_text_excerpt": "0123456789ABCDEFGHIJ",
    }
    result = {
        "schema": "onec_templates_analyze.v1",
        "status": "ok",
        "templates": [{"name": "Макет", "parts": [{"part_id": "x"}], "structure": structure, "analysis": adapter_server.analyze_template_structure(structure)}],
    }

    compact = adapter_server.apply_template_response_view(
        result,
        {"view": "summary", "max_cells": 1, "max_areas": 1},
        default_view="summary",
        map_mode=True,
    )

    assert compact["schema"] == "onec_templates_map.v1"
    assert compact["view"] == "summary"
    template = compact["templates"][0]
    assert "parts" not in template
    assert template["structure"]["capacity_dimensions"] == {"rows": 10, "columns": 5}
    assert template["structure"]["used_dimensions"]["columns"] == 1
    assert template["structure"]["format_dimensions"]["evidence"] == ["column_widths"]
    assert template["structure"]["counts"]["cells"] == 3
    assert template["structure"]["counts"]["cell_coordinate_hints"] == 2
    assert template["structure"]["counts"]["cell_style_coordinate_hints"] == 1
    assert len(template["structure"]["samples"]["cells"]) == 1
    assert len(template["structure"]["samples"]["cell_coordinate_hints"]) == 1
    assert template["analysis"]["counts"]["cells"] == 3
    assert template["analysis"]["counts"]["cell_coordinate_hints"] == 2
    assert "cells_sample" not in template["analysis"]

    hints_compact = adapter_server.apply_template_response_view(
        result,
        {"view": "summary", "sections": "coordinate_hints", "max_cells": 1},
        default_view="summary",
        map_mode=True,
    )

    hints_template = hints_compact["templates"][0]
    assert len(hints_template["structure"]["cell_coordinate_hints"]) == 1
    assert len(hints_template["analysis"]["cell_coordinate_hints"]) == 1
    assert len(hints_template["analysis"]["cell_style_coordinate_hints"]) == 1

    diagnostics_compact = adapter_server.apply_template_response_view(
        result,
        {"view": "summary", "sections": "moxel_records", "max_moxel_records": 1},
        default_view="summary",
        map_mode=True,
    )

    diagnostics = diagnostics_compact["templates"][0]["structure"]["moxel_record_diagnostics"][0]
    assert len(diagnostics["head_counts"]) == 1
    assert len(diagnostics["head_samples"]) == 1
    assert diagnostics["head_samples"][0]["samples"][0]["tree_position"] == "$"
    assert len(diagnostics["top_level_records"]) == 1
    assert diagnostics["top_level_record_summary"]["total"] == 1
    assert diagnostics["top_level_record_summary"]["head_counts"] == [{"head": 8, "count": 1}]
    assert len(diagnostics["top_level_shapes"]) == 1
    assert len(diagnostics["top_level_shape_candidates"]) == 1
    assert diagnostics["top_level_candidate_summary"]["returned_count"] == 1
    assert len(diagnostics["samples"]) == 1
    assert len(diagnostics["coordinate_like_samples"]) == 1

    window_compact = adapter_server.apply_template_response_view(
        result,
        {
            "view": "summary",
            "sections": "moxel_records",
            "max_moxel_records": 10,
            "moxel_record_start": 2,
            "moxel_record_end": 2,
        },
        default_view="summary",
        map_mode=True,
    )

    window_diagnostics = window_compact["templates"][0]["structure"]["moxel_record_diagnostics"][0]
    assert window_diagnostics["top_level_window"] == {"start": 2, "end": 2, "source": "moxel_record_start/moxel_record_end"}
    assert window_diagnostics["top_level_records"] == [{"tree_position": "$.2", "head": 16, "numeric_items": [16, 2, 10]}]
    assert window_diagnostics["top_level_record_summary"]["position_range"] == {"start": 2, "end": 2}

    head_compact = adapter_server.apply_template_response_view(
        result,
        {
            "view": "summary",
            "sections": "moxel_records",
            "max_moxel_records": 10,
            "moxel_record_heads": "16",
        },
        default_view="summary",
        map_mode=True,
    )

    head_diagnostics = head_compact["templates"][0]["structure"]["moxel_record_diagnostics"][0]
    assert head_diagnostics["top_level_head_filter"] == [16]
    assert head_diagnostics["top_level_records"] == [{"tree_position": "$.2", "head": 16, "numeric_items": [16, 2, 10]}]

    context_compact = adapter_server.apply_template_response_view(
        result,
        {
            "view": "summary",
            "sections": "moxel_records",
            "max_moxel_records": 10,
            "moxel_record_heads": [16],
            "moxel_record_context": 1,
        },
        default_view="summary",
        map_mode=True,
    )

    context_diagnostics = context_compact["templates"][0]["structure"]["moxel_record_diagnostics"][0]
    assert context_diagnostics["top_level_context"] == {"radius": 1, "source": "moxel_record_context", "match_field": "match"}
    assert context_diagnostics["top_level_records"] == [
        {"tree_position": "$.1", "head": 8, "numeric_items": [8, 1], "match": False},
        {"tree_position": "$.2", "head": 16, "numeric_items": [16, 2, 10], "match": True},
        {"tree_position": "$.3", "head": 24, "numeric_items": [24, 3], "match": False},
    ]
    assert context_diagnostics["top_level_record_summary"]["matched_count"] == 1

    undecoded_compact = adapter_server.apply_template_response_view(
        result,
        {
            "view": "summary",
            "sections": "undecoded_evidence,payload,tree_root",
            "max_strings": 1,
            "max_moxel_records": 1,
            "max_excerpt_chars": 10,
        },
        default_view="summary",
        map_mode=True,
    )

    undecoded_structure = undecoded_compact["templates"][0]["structure"]
    assert undecoded_structure["payload"] == {"compression": "raw_deflate", "encoding": "cp1251"}
    assert undecoded_structure["tree_root_summary"] == {"type": "list", "items_count": 3, "head": "8"}
    assert undecoded_structure["undecoded_evidence"]["payload"] == {"compression": "raw_deflate", "encoding": "cp1251"}
    assert undecoded_structure["undecoded_evidence"]["tree_root_summary"] == {"type": "list", "items_count": 3, "head": "8"}
    assert undecoded_structure["undecoded_evidence"]["strings_sample"] == ["Строка"]
    assert undecoded_structure["undecoded_evidence"]["moxel_text_excerpt"] == "0123456789"
    assert len(undecoded_structure["undecoded_evidence"]["coordinate_like_samples"]) == 1

    candidate_focus_compact = adapter_server.apply_template_response_view(
        result,
        {
            "view": "summary",
            "sections": "moxel_records",
            "max_moxel_records": 10,
            "moxel_candidate_rank": 1,
        },
        default_view="summary",
        map_mode=True,
    )

    candidate_focus = candidate_focus_compact["templates"][0]["structure"]["moxel_record_diagnostics"][0]
    assert candidate_focus["top_level_candidate_focus"]["rank"] == 1
    assert candidate_focus["top_level_candidate_focus"]["window_index"] == 1
    assert candidate_focus["top_level_candidate_focus"]["status"] == "ok"
    assert candidate_focus["top_level_head_filter"] == [16]
    assert candidate_focus["top_level_records"] == [
        {"tree_position": "$.1", "head": 8, "numeric_items": [8, 1], "match": False},
        {"tree_position": "$.2", "head": 16, "numeric_items": [16, 2, 10], "match": True},
        {"tree_position": "$.3", "head": 24, "numeric_items": [24, 3], "match": False},
    ]
    assert candidate_focus["top_level_record_summary"]["numeric_field_summary"][0]["head"] == 8

    candidate_second_window_compact = adapter_server.apply_template_response_view(
        result,
        {
            "view": "summary",
            "sections": "moxel_records",
            "max_moxel_records": 10,
            "moxel_candidate_rank": 1,
            "moxel_candidate_window_index": 2,
        },
        default_view="summary",
        map_mode=True,
    )

    candidate_second_window = candidate_second_window_compact["templates"][0]["structure"]["moxel_record_diagnostics"][0]
    assert candidate_second_window["top_level_candidate_focus"]["rank"] == 1
    assert candidate_second_window["top_level_candidate_focus"]["window_index"] == 2
    assert candidate_second_window["top_level_candidate_focus"]["status"] == "ok"
    assert candidate_second_window["top_level_window"] == {"start": 2, "end": 2, "source": "moxel_record_start/moxel_record_end"}
    assert candidate_second_window["top_level_records"] == [{"tree_position": "$.2", "head": 16, "numeric_items": [16, 2, 10]}]

    candidate_missing_window_compact = adapter_server.apply_template_response_view(
        result,
        {
            "view": "summary",
            "sections": "moxel_records",
            "max_moxel_records": 10,
            "moxel_candidate_rank": 1,
            "moxel_candidate_window_index": 3,
        },
        default_view="summary",
        map_mode=True,
    )

    candidate_missing_window = candidate_missing_window_compact["templates"][0]["structure"]["moxel_record_diagnostics"][0]
    assert candidate_missing_window["top_level_candidate_focus"]["rank"] == 1
    assert candidate_missing_window["top_level_candidate_focus"]["window_index"] == 3
    assert candidate_missing_window["top_level_candidate_focus"]["status"] == "window_not_found"
    assert candidate_missing_window["top_level_candidate_focus"]["available_windows"] == 2

    reason_filter_compact = adapter_server.apply_template_response_view(
        result,
        {
            "view": "summary",
            "sections": "moxel_records",
            "max_moxel_records": 10,
            "moxel_candidate_reasons": "rare_shape",
        },
        default_view="summary",
        map_mode=True,
    )

    reason_filter = reason_filter_compact["templates"][0]["structure"]["moxel_record_diagnostics"][0]
    assert reason_filter["top_level_candidate_reason_filter"] == ["rare_shape"]
    assert [candidate["rank"] for candidate in reason_filter["top_level_shape_candidates"]] == [1, 2]

    min_score_compact = adapter_server.apply_template_response_view(
        result,
        {
            "view": "summary",
            "sections": "moxel_records",
            "max_moxel_records": 10,
            "moxel_candidate_min_score": 6,
        },
        default_view="summary",
        map_mode=True,
    )

    min_score = min_score_compact["templates"][0]["structure"]["moxel_record_diagnostics"][0]
    assert min_score["top_level_candidate_min_score"] == 6
    assert min_score["top_level_candidate_summary"]["total"] == 2
    assert min_score["top_level_candidate_summary"]["returned_count"] == 0
    assert min_score["top_level_shape_candidates"] == []

    candidate_head_compact = adapter_server.apply_template_response_view(
        result,
        {
            "view": "summary",
            "sections": "moxel_records",
            "max_moxel_records": 10,
            "moxel_candidate_heads": "8",
        },
        default_view="summary",
        map_mode=True,
    )

    candidate_head = candidate_head_compact["templates"][0]["structure"]["moxel_record_diagnostics"][0]
    assert candidate_head["top_level_candidate_head_filter"] == [8]
    assert [candidate["rank"] for candidate in candidate_head["top_level_shape_candidates"]] == [2]

    candidate_window_compact = adapter_server.apply_template_response_view(
        result,
        {
            "view": "summary",
            "sections": "moxel_records",
            "max_moxel_records": 10,
            "moxel_candidate_start": 2,
            "moxel_candidate_end": 2,
        },
        default_view="summary",
        map_mode=True,
    )

    candidate_window = candidate_window_compact["templates"][0]["structure"]["moxel_record_diagnostics"][0]
    assert candidate_window["top_level_candidate_window_filter"] == {
        "start": 2,
        "end": 2,
        "source": "moxel_candidate_start/moxel_candidate_end",
    }
    assert [candidate["rank"] for candidate in candidate_window["top_level_shape_candidates"]] == [1]


def test_templates_read_validates_moxel_candidate_window_index() -> None:
    payload = {
        "base_id": "upo_test",
        "table": "ConfigCAS",
        "file_name": "abc",
        "moxel_candidate_rank": 1,
        "moxel_candidate_window_index": 0,
    }

    result = adapter_server.validate_templates_read_payload(payload, "templates.map")

    assert result["status"] == "invalid_argument"
    assert result["argument"] == "moxel_candidate_window_index"


def test_moxel_top_level_record_summary_includes_numeric_field_hints() -> None:
    records = [
        {"tree_position": "$.10", "head": 100, "numeric_items": [100, 2, 84, 0], "match": False},
        {"tree_position": "$.11", "head": 100, "numeric_items": [100, 2, 90, 1], "match": True},
        {"tree_position": "$.12", "head": 100, "numeric_items": [100, 2, 120, 1], "match": True},
    ]

    summary = adapter_server.summarize_moxel_top_level_records(records)

    numeric_summary = summary["numeric_field_summary"][0]
    assert numeric_summary["head"] == 100
    assert numeric_summary["varying_fields"][0]["index"] == 2
    assert numeric_summary["field_hints"] == [
        {
            "index": 2,
            "kind": "coordinate_or_offset_like",
            "confidence": "low",
            "reason": "field is non-negative, varies across records, and has a wider numeric span",
            "min": 84,
            "max": 120,
            "values": [84, 90, 120],
        },
        {
            "index": 3,
            "kind": "flag_like",
            "confidence": "low",
            "reason": "field varies only between 0 and 1 in the returned records",
            "values": [0, 1],
        },
    ]
    assert numeric_summary["numeric_field_matrix"] == {
        "fields": [2, 3],
        "rows": [
            {"tree_position": "$.10", "values": {"2": 84, "3": 0}, "match": False},
            {"tree_position": "$.11", "values": {"2": 90, "3": 1}, "match": True},
            {"tree_position": "$.12", "values": {"2": 120, "3": 1}, "match": True},
        ],
        "field_runs": [
            {
                "field": 2,
                "runs": [
                    {"value": 84, "start": "$.10", "end": "$.10", "rows": 1, "matched_count": 0},
                    {"value": 90, "start": "$.11", "end": "$.11", "rows": 1, "matched_count": 1},
                    {"value": 120, "start": "$.12", "end": "$.12", "rows": 1, "matched_count": 1},
                ],
            },
            {
                "field": 3,
                "runs": [
                    {"value": 0, "start": "$.10", "end": "$.10", "rows": 1, "matched_count": 0},
                    {"value": 1, "start": "$.11", "end": "$.12", "rows": 2, "matched_count": 2},
                ],
            },
        ],
        "field_transitions": [
            {
                "field": 2,
                "transitions": [
                    {
                        "from": 84,
                        "to": 90,
                        "before": "$.10",
                        "after": "$.11",
                        "before_rows": 1,
                        "after_rows": 1,
                        "before_matched_count": 0,
                        "after_matched_count": 1,
                    },
                    {
                        "from": 90,
                        "to": 120,
                        "before": "$.11",
                        "after": "$.12",
                        "before_rows": 1,
                        "after_rows": 1,
                        "before_matched_count": 1,
                        "after_matched_count": 1,
                    },
                ],
            },
            {
                "field": 3,
                "transitions": [
                    {
                        "from": 0,
                        "to": 1,
                        "before": "$.10",
                        "after": "$.11",
                        "before_rows": 1,
                        "after_rows": 2,
                        "before_matched_count": 0,
                        "after_matched_count": 2,
                    }
                ],
            },
        ],
    }


def test_extract_moxel_cell_style_candidates_from_inline_text_cell() -> None:
    tree = parse_brace_text(
        '{8,1,12,{"ru","ru",1,1,"ru","Русский","Русский",1},{128,72},'
        '{0,0},{0,0},1,2,1,3,0,1,1,'
        '{16,1,{1,1,{"ru","пример ячейка 4 2"}},0},'
        '{2,0,00000000-0000-0000-0000-000000000000,1,1,2}}'
    )

    candidates = adapter_server.extract_moxel_cell_style_candidates_from_tree(tree)

    assert len(candidates) == 1
    candidate = candidates[0]
    assert candidate["type_code"] == 16
    assert candidate["cell_id"] == 1
    assert candidate["text"] == "пример ячейка 4 2"
    assert candidate["texts"] == ["пример ячейка 4 2"]
    assert candidate["source"] == "moxel_inline_text_cell"
    assert candidate["confidence"] == "low"
    assert candidate["coordinate_hints"] == {
        "one_based": {"column": 2},
        "zero_based": {"column": 1},
        "confidence": "high",
        "source": "moxel_schema_rule:inline_text_column_from_last_preceding_scalar_plus_one",
    }
    assert candidate["style_evidence"]["last_7_preceding_values"] == ["1", "2", "1", "3", "0", "1", "1"]
    assert candidate["style_evidence"]["immediate_preceding_values"] == ["1", "2", "1", "3", "0", "1", "1"]
    assert candidate["next_moxel_record"]["head"] == "2"
    assert candidate["next_moxel_record"]["scalar_prefix"] == ["2", "0", "00000000-0000-0000-0000-000000000000", "1", "1", "2"]
    assert adapter_server.moxel_structure_counts({"cell_style_candidates": candidates})["cell_style_coordinate_hints"] == 1


def test_extract_moxel_cell_coordinate_hints_links_style_column_to_decoded_row() -> None:
    hints = adapter_server.extract_moxel_cell_coordinate_hints(
        [{"row": 4, "column": 9, "cell_id": 1, "text": "пример ячейка 4 2", "one_based": {"row": 4, "column": 9}, "zero_based": {"row": 3, "column": 8}}],
        [
            {
                "cell_id": 1,
                "text": "пример ячейка 4 2",
                "tree_position": "$.20",
                "coordinate_hints": {
                    "one_based": {"column": 2},
                    "zero_based": {"column": 1},
                    "confidence": "high",
                    "source": "moxel_schema_rule:inline_text_column_from_last_preceding_scalar_plus_one",
                },
            }
        ],
    )

    assert hints == [
        {
            "text": "пример ячейка 4 2",
            "cell_id": 1,
            "tree_position": "$.20",
            "one_based": {"row": 4, "column": 2},
            "zero_based": {"row": 3, "column": 1},
            "confidence": "high",
            "source": "moxel_inline_text_coordinate_hint",
            "evidence": {
                "column": "moxel_schema_rule:inline_text_column_from_last_preceding_scalar_plus_one",
                "row": "matched_decoded_cell_row",
                "matched_cell": {"row": 4, "column": 9, "cell_id": 1, "source": None},
            },
        }
    ]


def test_extract_moxel_cell_style_candidates_from_nested_inline_text_cell() -> None:
    tree = parse_brace_text(
        '{8,1,12,{99,{4,0,1,4,{16,7,{1,1,{"ru","nested style text"}},0}}}}'
    )

    candidates = adapter_server.extract_moxel_cell_style_candidates_from_tree(tree)

    assert len(candidates) == 1
    assert candidates[0]["tree_position"] == "$.3.1.4"
    assert candidates[0]["text"] == "nested style text"
    assert candidates[0]["coordinate_hints"]["one_based"] == {"column": 5}


def test_extract_moxel_cells_from_tree_uses_row_run_columns() -> None:
    tree = parse_brace_text(
        '{9,0,3,1,'
        '{16,0,{1,1,{"ru","10-2"}},0},2,'
        '{16,0,{1,1,{"ru","10-3"}},0},3,'
        '{16,0,{1,1,{"ru","10-4"}},0}}'
    )

    cells = adapter_server.extract_moxel_cells_from_tree(tree)

    assert [cell["text"] for cell in cells] == ["10-2", "10-3", "10-4"]
    assert [(cell["one_based"]["row"], cell["one_based"]["column"]) for cell in cells] == [
        (10, 2),
        (10, 3),
        (10, 4),
    ]


def test_extract_moxel_cells_from_tree_uses_flag_as_first_column_for_single_cell_row() -> None:
    tree = parse_brace_text('{4,0,1,2,{16,2,{1,1,{"ru","Ячейка 5 - 3"}},0}}')

    cells = adapter_server.extract_moxel_cells_from_tree(tree)

    assert len(cells) == 1
    assert cells[0]["text"] == "Ячейка 5 - 3"
    assert cells[0]["one_based"] == {"row": 5, "column": 3}


def test_extract_moxel_cells_from_tree_accepts_next_row_header_with_zero_flag() -> None:
    tree = parse_brace_text(
        '{10,0,3,1,'
        '{16,0,{1,1,{"ru","11-2"}},0},2,'
        '{16,0,{1,1,{"ru","11-3"}},0},3,'
        '{16,0,{1,1,{"ru","11-4"}},0},'
        '11,0,4,0,'
        '{16,0,{1,1,{"ru","12-1"}},0},1,'
        '{16,0,{1,1,{"ru","12-2"}},0},2,'
        '{16,0,{1,1,{"ru","12-3"}},0},3,'
        '{16,0,{1,1,{"ru","12-4"}},0}}'
    )

    cells = adapter_server.extract_moxel_cells_from_tree(tree)

    assert [(cell["text"], cell["one_based"]["row"], cell["one_based"]["column"]) for cell in cells] == [
        ("11-2", 11, 2),
        ("11-3", 11, 3),
        ("11-4", 11, 4),
        ("12-1", 12, 1),
        ("12-2", 12, 2),
        ("12-3", 12, 3),
        ("12-4", 12, 4),
    ]


def test_extract_moxel_cells_from_tree_falls_back_to_legacy_cell_column_pairs() -> None:
    tree = parse_brace_text(
        '{9,0,2,0,'
        '{16,0,{1,1,{"ru","legacy-1"}},0},1,'
        '{16,0,{1,1,{"ru","legacy-2"}},0},4}'
    )

    cells = adapter_server.extract_moxel_cells_from_tree(tree)

    assert [cell["text"] for cell in cells] == ["legacy-1", "legacy-2"]
    assert [(cell["one_based"]["row"], cell["one_based"]["column"]) for cell in cells] == [
        (10, 2),
        (10, 5),
    ]


def test_payload_public_preview_hides_stream_coordinates() -> None:
    preview = payload_public_preview(
        {
            "stream_blocks": [
                {
                    "header_offset": 12,
                    "data_offset": 30,
                    "sha1": "abc",
                    "encoding": "utf-8-sig",
                    "text_preview": "Процедура Команда()",
                    "has_bsl_marker": True,
                }
            ],
            "base64_blocks": [
                {
                    "block_length": 100,
                    "sha1": "def",
                    "encoding": "utf-8",
                    "text_preview": "<html>",
                    "has_html_marker": True,
                }
            ],
        }
    )

    assert preview["streams"][0]["text_preview"] == "Процедура Команда()"
    assert preview["streams"][0]["has_bsl_marker"] is True
    assert "header_offset" not in preview["streams"][0]
    assert "sha1" not in preview["streams"][0]
    assert preview["base64"][0]["has_html_marker"] is True
    assert "block_length" not in preview["base64"][0]


def test_payload_diff_compares_inline_text_tree_and_strings() -> None:
    result = adapter_server.payload_diff(
        {
            "diagnostic": True,
            "before": {"text": '{1,"Пример"}'},
            "after": {"text": '{1,"Проверка"}'},
            "max_changes": 20,
            "max_text_diff_lines": 20,
        }
    )

    assert result["status"] == "changed"
    assert result["bytes"]["same"] is False
    assert result["text"]["same"] is False
    assert any("Пример" in line for line in result["text"]["diff_lines"])
    assert any("Проверка" in line for line in result["text"]["diff_lines"])
    assert result["tree"]["changes"] == [{"path": "$.1", "old": "Пример", "new": "Проверка"}]
    assert result["strings"]["changes"] == [{"index": 0, "old": "Пример", "new": "Проверка"}]


def test_payload_diff_reads_live_sources(monkeypatch: pytest.MonkeyPatch) -> None:
    payloads = {
        ("ConfigSave", "before"): '{1,"До"}'.encode("utf-8-sig"),
        ("ConfigSave", "after"): '{1,"После"}'.encode("utf-8-sig"),
    }

    def fake_read_storage_file_bytes(base_id: str, table: str, file_name: str, *, timeout_seconds: int = 30):
        return payloads[(table, file_name)], {"database": base_id}, None

    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)

    result = adapter_server.payload_diff(
        {
            "diagnostic": True,
            "base_id": "upo_test",
            "before": {"table": "ConfigSave", "file_name": "before"},
            "after": {"table": "ConfigSave", "file_name": "after"},
            "include_evidence": False,
        }
    )

    assert result["status"] == "changed"
    assert result["source"]["before"]["file_name"] == "before"
    assert result["source"]["after"]["file_name"] == "after"
    assert result["tree"]["changes"][0] == {"path": "$.1", "old": "До", "new": "После"}
    assert "evidence" not in result


def test_payload_diff_requires_diagnostic() -> None:
    result = adapter_server.payload_diff({"before": {"text": "{1}"}, "after": {"text": "{2}"}})

    assert result["status"] == "invalid_argument"
    assert result["argument"] == "diagnostic"


def test_payload_public_undecoded_evidence_hides_block_coordinates() -> None:
    evidence = payload_public_undecoded_evidence(
        {
            "status": "ok",
            "role": "bsl_module_payload",
            "compression": "raw_deflate",
            "encoding": "cp1251",
            "raw_bytes": 10,
            "payload_bytes": 20,
            "sha1": "abc",
            "payload_sha1": "def",
            "markers": ["marker1", "marker2"],
            "root": {"root_marker": "1"},
            "strings_sample": ["Строка"],
            "counts": {"stream_blocks": 1, "base64_blocks": 1},
            "text": "0123456789ABCDE",
            "stream_blocks": [
                {
                    "header_offset": 12,
                    "data_offset": 30,
                    "sha1": "hidden",
                    "encoding": "utf-8-sig",
                    "text_preview": "Процедура Команда()",
                    "has_bsl_marker": True,
                }
            ],
            "base64_blocks": [
                {
                    "block_length": 100,
                    "sha1": "hidden2",
                    "encoding": "utf-8",
                    "text_preview": "<html>",
                    "has_html_marker": True,
                }
            ],
        },
        max_excerpt_chars=10,
    )

    assert evidence["payload"]["encoding"] == "cp1251"
    assert evidence["text_excerpt"] == "0123456789"
    assert evidence["stream_blocks_sample"][0]["has_bsl_marker"] is True
    assert "header_offset" not in evidence["stream_blocks_sample"][0]
    assert "sha1" not in evidence["stream_blocks_sample"][0]
    assert evidence["base64_blocks_sample"][0]["has_html_marker"] is True
    assert "block_length" not in evidence["base64_blocks_sample"][0]


def test_payload_public_undecoded_evidence_raw_requires_storage_details() -> None:
    classification = {
        "status": "ok",
        "role": "bsl_module_payload",
        "compression": "none",
        "encoding": "utf-8",
        "raw_bytes": 10,
        "payload_bytes": 10,
        "sha1": "payload-sha1",
        "payload_sha1": "payload-sha1",
        "stream_blocks": [
            {
                "header_offset": 12,
                "data_offset": 30,
                "declared_1": 8,
                "declared_2": 8,
                "bytes": 8,
                "sha1": "block-sha1",
                "encoding": "utf-8",
                "text_preview": "Процедура",
                "text": "Процедура Команда()\nКонецПроцедуры",
                "has_bsl_marker": True,
            }
        ],
        "base64_blocks": [],
    }

    public_raw = payload_public_undecoded_evidence(classification, mode="raw", allow_storage_details=False)
    storage_raw = payload_public_undecoded_evidence(classification, mode="raw", allow_storage_details=True)

    assert "header_offset" not in public_raw["stream_blocks_sample"][0]
    assert public_raw["stream_blocks_sample"][0]["text_excerpt"].startswith("Процедура")
    assert storage_raw["stream_blocks_sample"][0]["header_offset"] == 12
    assert storage_raw["stream_blocks_sample"][0]["sha1"] == "block-sha1"


def test_metadata_object_parts_exposes_undecoded_evidence_without_storage(monkeypatch: pytest.MonkeyPatch) -> None:
    payload_text = '{1,"Тест"}'
    payload_bytes = compress_payload(payload_text.encode("cp1251"), "raw_deflate")

    monkeypatch.setattr(
        adapter_server,
        "resolve_object_guid",
        lambda *args, **kwargs: ("guid-1", "Document", {"guid": "guid-1", "kind": "Document"}, None),
    )
    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda *args, **kwargs: {"status": "ok", "files": [{"FileName": "guid-1"}]},
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_files_bytes",
        lambda *args, **kwargs: ({"guid-1": payload_bytes}, {"database": "db"}, None),
    )

    result = adapter_server.metadata_object_parts({"base_id": "upo_test", "guid": "guid-1", "include_text": True})

    assert result["status"] == "ok"
    part = result["parts"][0]
    assert part["content_kind"] == "metadata"
    assert part["undecoded_evidence"]["payload"]["compression"] == "raw_deflate"
    assert part["undecoded_evidence"]["strings_sample"] == ["Тест"]


def test_metadata_object_parts_raw_evidence_exposes_storage_when_requested(monkeypatch: pytest.MonkeyPatch) -> None:
    payload_bytes = b"\r\n00000004 00000004 7fffffff \r\ntext"

    monkeypatch.setattr(
        adapter_server,
        "resolve_object_guid",
        lambda *args, **kwargs: ("guid-1", "Document", {"guid": "guid-1", "kind": "Document"}, None),
    )
    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda *args, **kwargs: {"status": "ok", "files": [{"FileName": "guid-1"}]},
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_files_bytes",
        lambda *args, **kwargs: ({"guid-1": payload_bytes}, {"database": "db"}, None),
    )

    result = adapter_server.metadata_object_parts(
        {
            "base_id": "upo_test",
            "guid": "guid-1",
            "include_storage": True,
            "evidence_mode": "raw",
        }
    )

    assert result["status"] == "ok"
    assert result["parts"][0]["undecoded_evidence"]["stream_blocks_sample"][0]["header_offset"] == 0
    assert result["parts"][0]["undecoded_evidence"]["stream_blocks_sample"][0]["bytes"] == 4


def test_metadata_form_decode_returns_undecoded_evidence_for_raw_binary(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda *args, **kwargs: (b"\x00\x01\x02raw-binary", {"database": "db"}, None),
    )

    result = adapter_server.metadata_form_decode({"base_id": "upo_test", "file_name": "form-guid"})

    assert result["status"] == "undecodable"
    assert result["undecoded_evidence"]["payload"]["raw_bytes"] == len(b"\x00\x01\x02raw-binary")
    assert result["undecoded_evidence"]["role"] in {"binary_or_unknown_payload", "stream_container", "brace_payload", "form_payload", "metadata_payload", "template_payload", "help_or_html_payload", "bsl_module_payload"}


def test_mcp_full_sections_validation() -> None:
    payload: dict[str, object] = {"base_id": "upo_test", "sections": "forms"}
    error = validate_metadata_object_full_sections(payload)
    assert error is not None
    assert error["status"] == "invalid_argument"
    assert error["argument"] == "sections"

    payload = {"sections": ["forms", "card", "forms"]}
    error = validate_metadata_object_full_sections(payload)
    assert error is None
    assert payload["sections"] == ["forms", "card"]

    payload = {"sections": []}
    error = validate_metadata_object_full_sections(payload)
    assert error is not None
    assert error["status"] == "invalid_argument"

    payload = {"sections": ["unknown"]}
    error = validate_metadata_object_full_sections(payload)
    assert error is not None
    assert error["status"] == "invalid_argument"

    payload = {"sections": [1]}
    error = validate_metadata_object_full_sections(payload)
    assert error is not None
    assert error["status"] == "invalid_argument"

    payload = {"sections": ["all"]}
    error = validate_metadata_object_full_sections(payload)
    assert error is None
    assert payload["sections"] == ["card", "semantic", "modules", "templates", "forms", "commands"]


def test_adapter_full_sections_validation() -> None:
    payload: dict[str, object] = {"base_id": "upo_test", "sections": "forms"}
    error = validate_metadata_object_full_payload(payload)
    assert error is not None
    assert error["status"] == "invalid_argument"
    assert error["argument"] == "sections"

    payload = {"base_id": "upo_test", "sections": ["forms", "card", "forms"]}
    error = validate_metadata_object_full_payload(payload)
    assert error is None
    assert payload["_sections"] == ["forms", "card"]

    payload = {"base_id": "upo_test", "sections": []}
    error = validate_metadata_object_full_payload(payload)
    assert error is not None
    assert error["status"] == "invalid_argument"
    assert error["argument"] == "sections"

    payload = {"base_id": "upo_test", "sections": ["unknown"]}
    error = validate_metadata_object_full_payload(payload)
    assert error is not None
    assert error["status"] == "invalid_argument"
    assert error["argument"] == "sections"

    payload = {"base_id": "upo_test", "sections": [1]}
    error = validate_metadata_object_full_payload(payload)
    assert error is not None
    assert error["status"] == "invalid_argument"
    assert error["argument"] == "sections"

    payload = {"base_id": "upo_test", "sections": ["all"]}
    error = validate_metadata_object_full_payload(payload)
    assert error is None
    assert payload["_sections"] == ["card", "semantic", "modules", "templates", "forms", "commands"]


def test_form_element_write_requires_saved_state_opt_in() -> None:
    result = adapter_server.metadata_form_element_write(
        {"base_id": "upo_test", "table": "ConfigSave", "form_guid": "form-guid", "element": "Кнопка", "property": "title", "value": "Новый"}
    )

    assert result["status"] == "invalid_argument"
    assert result["argument"] == "allow_saved_state_write"


def test_form_element_write_rejects_active_config_table() -> None:
    result = adapter_server.metadata_form_element_write(
        {
            "base_id": "upo_test",
            "table": "Config",
            "form_guid": "form-guid",
            "element": "Кнопка",
            "allow_saved_state_write": True,
            "property": "title",
            "value": "Новый",
        }
    )

    assert result["status"] == "invalid_argument"
    assert result["argument"] == "table"


def test_form_element_write_uses_extension_saved_state_for_name_first_selector(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_resolve(payload: dict[str, Any]) -> dict[str, Any]:
        seen["table"] = payload["table"]
        return {"status": "not_found", "base_id": payload["base_id"], "diagnostics": {"message": "stop"}}

    monkeypatch.setattr(adapter_server, "metadata_form_write_target_resolve", fake_resolve)
    result = adapter_server.metadata_form_element_write(
        {
            "base_id": "upo_test",
            "extension": "фс_Отчеты1",
            "ref": "Report.фс_ПродовольственнаяКорзина",
            "form": "ФормаОтчетаПродкорзина",
            "element": "КомпоновщикНастроекПользовательскиеНастройки",
            "property": "Группировка",
            "value": "Horizontal",
            "allow_saved_state_write": True,
        }
    )

    assert result["status"] == "not_found"
    assert seen["table"] == "ConfigCASSave"


def test_repository_layer_connection_set_resolves_extension_name_before_saving(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "8e02accd-8a34-11f1-8294-005056b0d483"
    saved: dict[str, Any] = {}
    monkeypatch.setattr(
        adapter_server,
        "extension_filter_to_guid",
        lambda base_id, extension, method: (extension_guid, None),
    )
    monkeypatch.setattr(adapter_server, "sql_admin_load", lambda: {"upo_test": {"server": "sql", "database": "upo_test", "user": "sa"}})
    monkeypatch.setattr(adapter_server, "sql_admin_save", lambda config: saved.setdefault("config", config))
    monkeypatch.setattr(
        adapter_server.repository_control,
        "status",
        lambda payload: {"status": "manual_confirmation_required", "repository": {"mode": "manual"}},
    )

    result = adapter_server.repository_layer_connection_set(
        {
            "base_id": "upo_test",
            "extension": "фс_Отчеты1",
            "connection_state": "configured",
            "confirm_repository_connection_change": True,
        }
    )

    layer = saved["config"]["upo_test"]["development_layers"][f"extension:{extension_guid}"]
    assert result["status"] == "saved"
    assert result["layer_id"] == f"extension:{extension_guid}"
    assert layer["repository"]["mode"] == "manual"
    assert layer["repository"]["connection_state"] == "configured"


def test_repository_layer_connection_plan_is_name_first_and_read_only(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "8e02accd-8a34-11f1-8294-005056b0d483"
    monkeypatch.setattr(adapter_server, "extension_filter_to_guid", lambda base_id, extension, method: (extension_guid, None))
    monkeypatch.setattr(
        adapter_server.repository_control,
        "development_layer_config",
        lambda base_id, layer_id: (None, {"status": "layer_not_configured"}),
    )

    result = adapter_server.repository_layer_connection_plan(
        {"base_id": "upo_test", "extension": "фс_Отчеты1", "connection_state": "configured"}
    )

    assert result["status"] == "needs_configuration"
    assert result["target"] == {"kind": "extension", "extension": "фс_Отчеты1"}
    assert result["apply"]["payload"] == {
        "base_id": "upo_test",
        "extension": "фс_Отчеты1",
        "connection_state": "configured",
        "confirm_repository_connection_change": True,
    }
    assert result["diagnostics"]["read_only"] is True


def test_form_element_write_keeps_owner_guid_out_of_form_guid_resolution(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}
    owner_guid = "22222222-2222-2222-2222-222222222222"

    def fake_resolve(payload: dict[str, Any]) -> dict[str, Any]:
        seen["resolve_payload"] = payload
        return {
            "schema": "onec_form_write_target_resolution.v1",
            "status": "not_found",
            "base_id": payload["base_id"],
            "diagnostics": {"message": "test stop after selector normalization"},
        }

    monkeypatch.setattr(adapter_server, "metadata_form_write_target_resolve", fake_resolve)

    result = adapter_server.call_method(
        "metadata.form.element.write",
        {
            "base_id": "upo_test",
            "ref": "Обработки.ВторойОбмен",
            "object_guid": owner_guid,
            "form_name": "ФормаОбмена",
            "element_name": "КнопкаВыполнить",
            "property": "title",
            "value": "Выполнить",
            "allow_saved_state_write": True,
        },
    )

    assert result["status"] == "not_found"
    assert seen["resolve_payload"]["kind"] == "DataProcessor"
    assert seen["resolve_payload"]["name"] == "ВторойОбмен"
    assert seen["resolve_payload"]["guid"] == owner_guid
    assert seen["resolve_payload"]["form_name"] == "ФормаОбмена"
    assert seen["resolve_payload"].get("form_guid") is None


def test_form_element_write_help_exposes_owner_form_and_write_guards() -> None:
    methods = {
        row["name"]: row
        for row in adapter_server.call_method("help.methods", {})["methods"]
    }

    planner = methods["metadata.form.element.write"]
    apply = methods["metadata.form.element.write_apply"]
    for method in (planner, apply):
        assert method["selector_capabilities"]["accepts_ordinal"] is False
        assert method["selector_capabilities"]["form_selector_arguments"] == ["form", "form_name", "form_guid"]
        assert method["selector_capabilities"]["child_selector_arguments"] == [
            "element",
            "element_name",
            "command",
            "attribute",
        ]
        assert method["input_schema"]["properties"]["object_guid"]["type"] == "string"
        assert method["input_schema"]["properties"]["form_guid"]["type"] == "string"
    assert planner["selector_capabilities"]["write_guards"] == ["allow_saved_state_write", "proposal_only"]
    assert "allow_sql_saved_state_apply" in apply["selector_capabilities"]["write_guards"]


def test_form_target_move_resolves_nested_form_by_owner_before_decode(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}
    owner_guid = "22222222-2222-2222-2222-222222222222"

    def fake_resolve(payload: dict[str, Any]) -> dict[str, Any]:
        seen["resolve_payload"] = payload
        return {
            "schema": "onec_form_write_target_resolution.v1",
            "status": "not_found",
            "base_id": payload["base_id"],
            "diagnostics": {"message": "test stop after owner/form resolution"},
        }

    monkeypatch.setattr(adapter_server, "metadata_form_write_target_resolve", fake_resolve)

    result = adapter_server.call_method(
        "metadata.form.target.move",
        {
            "base_id": "upo_test",
            "ref": "Обработки.ВторойОбмен",
            "object_guid": owner_guid,
            "form_name": "ФормаОбмена",
            "from_element": "КнопкаСтарая",
            "to_element": "КнопкаНовая",
            "allow_saved_state_write": True,
        },
    )

    assert result["status"] == "not_found"
    assert seen["resolve_payload"]["kind"] == "DataProcessor"
    assert seen["resolve_payload"]["name"] == "ВторойОбмен"
    assert seen["resolve_payload"]["guid"] == owner_guid
    assert seen["resolve_payload"]["form_name"] == "ФормаОбмена"
    assert seen["resolve_payload"]["element"] == "КнопкаСтарая"
    assert seen["resolve_payload"].get("form_guid") is None


def test_form_write_target_resolve_preserves_legacy_guid_only_form_selector(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
        seen["decode_payload"] = payload
        return {"schema": "onec_form_decode.v1", "status": "not_found"}

    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)

    result = adapter_server.metadata_form_write_target_resolve(
        {
            "base_id": "upo_test",
            "table": "ConfigCASSave",
            "guid": "legacy-form-guid",
            "element": "Кнопка",
        }
    )

    assert result["status"] == "not_found"
    assert seen["decode_payload"]["form_guid"] == "legacy-form-guid"
    assert "guid" not in seen["decode_payload"]


def test_form_element_write_builds_saved_state_change_proposal(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
        seen["decode"] = payload
        return {
            "status": "ok",
            "source": {"kind": "live_sql", "table": "ConfigSave", "file_name": "form-guid.0"},
            "form": {"guid": "form-guid", "file_name": "form-guid.0"},
            "profile": {
                "items": [
                    {
                        "name": "СписокАрхивироватьДокументы",
                        "name_path": "1.27.5",
                        "id": "172",
                        "id_path": "1.27.1.0",
                        "title": "Архивировать",
                        "title_path": "1.27.6.2.1",
                        "path": "1.27",
                        "parameters": [
                            {"index": 7, "presentation": "Видимость", "value": "1"},
                        ],
                    }
                ]
            },
        }

    def fake_changes(payload: dict[str, Any]) -> dict[str, Any]:
        seen["changes"] = payload
        return {
            "schema": "onec_change_proposal.v1",
            "status": "accepted_for_review",
            "applied": False,
            "edits": payload["edits"],
            "source": payload["source"],
        }

    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
    monkeypatch.setattr(adapter_server, "changes_propose", fake_changes)

    result = adapter_server.metadata_form_element_write(
        {
            "base_id": "upo_test",
            "table": "ConfigSave",
            "form_guid": "form-guid",
            "element": "СписокАрхивироватьДокументы",
            "allow_saved_state_write": True,
            "edits": [
                {"property": "title", "value": "В архив"},
                {"property": "Видимость", "value": False},
            ],
        }
    )

    assert seen["decode"]["include_storage"] is True
    assert seen["decode"]["include_parameters"] is True
    assert seen["changes"]["source"] == {"base_id": "upo_test", "table": "ConfigSave", "file_name": "form-guid.0"}
    assert seen["changes"]["edits"] == [
        {"path": "1.27.6.2.1", "value": "В архив", "node_type": "auto", "property": "title", "old": "Архивировать"},
        {"path": "1.27.7", "value": "0", "node_type": "auto", "property": "Видимость", "old": "1"},
    ]
    assert result["status"] == "accepted_for_review"
    assert result["write_mode"]["sql_write_performed"] is False
    assert result["element"]["name"] == "СписокАрхивироватьДокументы"


def test_form_element_write_builds_saved_state_command_proposal(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "status": "ok",
            "source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": "form-guid.0"},
            "form": {"guid": "form-guid", "file_name": "form-guid.0"},
            "profile": {
                "items": [],
                "commands": [
                    {
                        "name": "КомандаПример1",
                        "id": "2",
                        "title": "Пример1",
                        "path": "5.3",
                        "id_path": "5.3.1.0",
                        "title_path": "5.3.3.2.1",
                        "parameters": [{"index": 3, "presentation": "Заголовок", "value": ""}],
                    }
                ],
            },
        }

    def fake_changes(payload: dict[str, Any]) -> dict[str, Any]:
        seen["changes"] = payload
        return {"schema": "onec_change_proposal.v1", "status": "accepted_for_review", "source": payload["source"], "edits": payload["edits"]}

    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
    monkeypatch.setattr(adapter_server, "changes_propose", fake_changes)

    result = adapter_server.metadata_form_element_write(
        {
            "base_id": "upo_test",
            "table": "ConfigCASSave",
            "file_name": "form-guid.0",
            "element": "КомандаПример1",
            "allow_saved_state_write": True,
            "edits": [{"property": "title", "value": "ПРОВЕРКА"}],
        }
    )

    assert seen["changes"]["edits"] == [
        {"path": "5.3.3.2.1", "value": "ПРОВЕРКА", "node_type": "auto", "property": "title", "old": "Пример1"}
    ]
    assert result["status"] == "accepted_for_review"
    assert result["element"]["section"] == "commands"
    assert result["element"]["name"] == "КомандаПример1"


def test_form_element_write_routes_empty_element_title_to_linked_command(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "status": "ok",
            "source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": "form-guid.0"},
            "form": {"guid": "form-guid", "file_name": "form-guid.0"},
            "profile": {
                "items": [
                    {
                        "name": "КомандаПример1",
                        "title": None,
                        "path": "1.10",
                        "title_path": "1.10.6",
                        "marker": "31",
                    }
                ],
                "commands": [
                    {
                        "name": "КомандаПример1",
                        "id": "2",
                        "title": "Пример1",
                        "path": "5.3",
                        "title_path": "5.3.3.2.1",
                    }
                ],
            },
        }

    def fake_changes(payload: dict[str, Any]) -> dict[str, Any]:
        seen["changes"] = payload
        return {"schema": "onec_change_proposal.v1", "status": "accepted_for_review", "source": payload["source"], "edits": payload["edits"]}

    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
    monkeypatch.setattr(adapter_server, "changes_propose", fake_changes)

    result = adapter_server.metadata_form_element_write(
        {
            "base_id": "upo_test",
            "table": "ConfigCASSave",
            "file_name": "form-guid.0",
            "element_path": "1.10",
            "allow_saved_state_write": True,
            "edits": [{"property": "Заголовок", "value": "ПРОВЕРКА"}],
        }
    )

    assert seen["changes"]["preserve_format"] is True
    assert seen["changes"]["edits"] == [
        {"path": "5.3.3.2.1", "value": "ПРОВЕРКА", "node_type": "auto", "property": "Заголовок", "old": "Пример1"}
    ]
    assert result["requested_element"]["section"] == "items"
    assert result["element"]["section"] == "commands"
    assert result["effective_sources"][0]["kind"] == "linked_command_title"


def test_group_visibility_write_uses_controlled_designer_variant_parameter() -> None:
    common = {
        "marker": "22",
        "type_name": "Группа",
        "parameters": [
            {"index": 10, "presentation": "Параметр 10", "value": "1"},
        ],
        "semantic": {
            "groups": {
                "Основные": [
                    {"name": "Видимость", "value": True, "parameter_index": 10, "source": "form_payload_container"}
                ]
            }
        },
    }
    group_form = {
        **common,
        "name": "ГруппаФорма",
        "path": "1.25.23",
        "parameters": [
            *common["parameters"],
            {"index": 26, "presentation": "Параметр 26", "value": "1"},
            {"index": 28, "presentation": "Параметр 28", "value": "1"},
        ],
    }
    nested_group = {
        **common,
        "name": "Группа1",
        "path": "1.25.23.23",
        "parameters": [
            *common["parameters"],
            {"index": 26, "presentation": "Параметр 26", "value": "77ffcc29-7f2d-4223-b22f-19666e7250ba"},
            {"index": 28, "presentation": "Параметр 28", "value": "1"},
        ],
    }

    group_edit, group_error = adapter_server.form_element_write_edit(group_form, {"property": "Видимость", "value": False}, 0)
    nested_edit, nested_error = adapter_server.form_element_write_edit(nested_group, {"property": "visible", "value": False}, 0)

    assert group_error is None
    assert nested_error is None
    assert group_edit["path"] == "1.25.23.26"
    assert group_edit["old"] == "1"
    assert group_edit["value"] == "0"
    assert nested_edit["path"] == "1.25.23.23.28"
    assert nested_edit["old"] == "1"
    assert nested_edit["value"] == "0"


def test_form_element_write_uses_property_registry_alias(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "status": "ok",
            "source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": "form-guid.0"},
            "form": {"guid": "form-guid", "file_name": "form-guid.0"},
            "profile": {
                "items": [
                    {
                        "name": "ПолеТест",
                        "title": "Поле",
                        "path": "1.10",
                        "parameters": [{"index": 7, "presentation": "Видимость", "value": "1"}],
                    }
                ],
            },
        }

    def fake_changes(payload: dict[str, Any]) -> dict[str, Any]:
        seen["changes"] = payload
        return {"schema": "onec_change_proposal.v1", "status": "accepted_for_review", "source": payload["source"], "edits": payload["edits"]}

    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
    monkeypatch.setattr(adapter_server, "changes_propose", fake_changes)

    result = adapter_server.metadata_form_element_write(
        {
            "base_id": "upo_test",
            "table": "ConfigCASSave",
            "file_name": "form-guid.0",
            "element": "ПолеТест",
            "allow_saved_state_write": True,
            "edits": [{"property": "visible", "value": False}],
        }
    )

    assert result["status"] == "accepted_for_review"
    assert seen["changes"]["edits"] == [
        {"path": "1.10.7", "value": "0", "node_type": "auto", "property": "visible", "old": "1"}
    ]
    assert result["form_element_edits"][0]["property"] == "visible"


def test_form_element_write_uses_command_button_location_alias(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "status": "ok",
            "source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": "form-guid.0"},
            "form": {"guid": "form-guid", "file_name": "form-guid.0"},
            "profile": {
                "items": [
                    {
                        "name": "КнопкаПровести",
                        "title": "Провести",
                        "path": "1.20",
                        "marker": "31",
                        "parameters": [
                            {"index": 14, "presentation": "ПоложениеВКоманднойПанели", "value": "0"},
                            {"index": 31, "presentation": "Доступность", "value": "1"},
                        ],
                    }
                ],
            },
        }

    def fake_changes(payload: dict[str, Any]) -> dict[str, Any]:
        seen["changes"] = payload
        return {"schema": "onec_change_proposal.v1", "status": "accepted_for_review", "source": payload["source"], "edits": payload["edits"]}

    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
    monkeypatch.setattr(adapter_server, "changes_propose", fake_changes)

    result = adapter_server.metadata_form_element_write(
        {
            "base_id": "upo_test",
            "table": "ConfigCASSave",
            "file_name": "form-guid.0",
            "element": "КнопкаПровести",
            "allow_saved_state_write": True,
            "edits": [{"property": "command_bar_location", "value": "2"}],
        }
    )

    assert result["status"] == "accepted_for_review"
    assert seen["changes"]["edits"] == [
        {"path": "1.20.14", "value": "2", "node_type": "auto", "property": "command_bar_location", "old": "0"}
    ]
    writable = result["form_element_edits"][0]
    assert writable["property"] == "command_bar_location"


def test_form_element_write_changes_button_to_local_form_command(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "status": "ok",
            "source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": "form-guid.0"},
            "form": {"guid": "form-guid", "file_name": "form-guid.0"},
            "profile": {
                "items": [
                    {
                        "name": "ФормаКомандаОбновить",
                        "title": "",
                        "path": "1.25.25.27",
                        "marker": "34",
                        "command_binding": {
                            "command_id": "1",
                            "group_guid": "409b9a53-7f7e-4178-86c1-33176c7c7a7a",
                            "command_id_path": "1.25.25.27.8.0",
                            "group_guid_path": "1.25.25.27.8.1",
                            "scope": "form",
                        },
                    }
                ],
                "commands": [
                    {"name": "КомандаПрименить", "id": "1", "path": "5.1"},
                    {"name": "КомандаПример1", "id": "2", "path": "5.2"},
                ],
            },
        }

    def fake_changes(payload: dict[str, Any]) -> dict[str, Any]:
        seen["changes"] = payload
        return {"schema": "onec_change_proposal.v1", "status": "accepted_for_review", "source": payload["source"], "edits": payload["edits"]}

    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
    monkeypatch.setattr(adapter_server, "changes_propose", fake_changes)

    result = adapter_server.metadata_form_element_write(
        {
            "base_id": "upo_test",
            "table": "ConfigCASSave",
            "file_name": "form-guid.0",
            "element": "ФормаКомандаОбновить",
            "allow_saved_state_write": True,
            "edits": [{"property": "ИмяКоманды", "value": "Form.Command.КомандаПример1"}],
        }
    )

    assert result["status"] == "accepted_for_review"
    assert seen["changes"]["edits"] == [
        {"path": "1.25.25.27.8.0", "value": "2", "node_type": "auto", "property": "ИмяКоманды", "old": "1"},
        {
            "path": "1.25.25.27.8.1",
            "value": "409b9a53-7f7e-4178-86c1-33176c7c7a7a",
            "node_type": "auto",
            "property": "ИмяКоманды",
            "old": "409b9a53-7f7e-4178-86c1-33176c7c7a7a",
        },
    ]
    assert result["semantic_diff"][0]["property"] == "ИмяКоманды"


def test_form_element_write_changes_button_to_standard_command(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "status": "ok",
            "source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": "form-guid.0"},
            "form": {"guid": "form-guid", "file_name": "form-guid.0"},
            "profile": {
                "items": [
                    {
                        "name": "ТЗИзменитьФорму",
                        "title": "",
                        "path": "1.25.23.25.62.23",
                        "marker": "34",
                        "command_binding": {
                            "command_id": "2",
                            "group_guid": "409b9a53-7f7e-4178-86c1-33176c7c7a7a",
                            "command_id_path": "1.25.23.25.62.23.8.0",
                            "group_guid_path": "1.25.23.25.62.23.8.1",
                            "scope": "form",
                        },
                    }
                ],
                "commands": [{"name": "КомандаПример1", "id": "2", "path": "5.2"}],
            },
        }

    def fake_changes(payload: dict[str, Any]) -> dict[str, Any]:
        seen["changes"] = payload
        return {"schema": "onec_change_proposal.v1", "status": "accepted_for_review", "source": payload["source"], "edits": payload["edits"]}

    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
    monkeypatch.setattr(adapter_server, "changes_propose", fake_changes)

    result = adapter_server.metadata_form_element_write(
        {
            "base_id": "upo_test",
            "table": "ConfigCASSave",
            "file_name": "form-guid.0",
            "element": "ТЗИзменитьФорму",
            "allow_saved_state_write": True,
            "edits": [{"property": "command_name", "value": "Form.StandardCommand.CustomizeForm"}],
        }
    )

    assert result["status"] == "accepted_for_review"
    assert seen["changes"]["edits"] == [
        {"path": "1.25.23.25.62.23.8.0", "value": "0", "node_type": "auto", "property": "command_name", "old": "2"},
        {
            "path": "1.25.23.25.62.23.8.1",
            "value": "198ea630-fda2-4cda-8a23-f999f4c67ee6",
            "node_type": "auto",
            "property": "command_name",
            "old": "409b9a53-7f7e-4178-86c1-33176c7c7a7a",
        },
    ]
    assert result["semantic_diff"][0]["property"] == "command_name"


def test_form_write_target_resolve_reports_registry_metadata_for_parameters(monkeypatch: pytest.MonkeyPatch) -> None:
    def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "status": "ok",
            "source": {"kind": "live_sql", "table": payload["table"], "file_name": payload["file_name"]},
            "form": {"file_name": payload["file_name"]},
            "profile": {
                "items": [
                    {
                        "name": "КнопкаПровести",
                        "title": "Провести",
                        "path": "1.20",
                        "marker": "31",
                        "parameters": [{"index": 14, "presentation": "ПоложениеВКоманднойПанели", "value": "0"}],
                    }
                ],
            },
        }

    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)

    result = adapter_server.metadata_form_write_target_resolve(
        {
            "base_id": "upo_test",
            "table": "ConfigCASSave",
            "file_name": "form-guid.0",
            "element": "КнопкаПровести",
            "property": "command_bar_location",
            "value": "2",
        }
    )

    assert result["status"] == "ok"
    assert result["display"]["actual"] == "0"
    assert result["write_target"]["path"] == "1.20.14"
    assert result["property"]["old"] == "0"
    parameter_rows = [row for row in result["writable_properties"] if row.get("path") == "1.20.14"]
    assert parameter_rows[0]["canonical_property"] == "command_bar_location"
    assert parameter_rows[0]["value_type"] == "enum_atom"


def test_form_write_target_resolve_prefers_element_selector_over_same_named_attribute(monkeypatch: pytest.MonkeyPatch) -> None:
    def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "status": "ok",
            "source": {"kind": "live_sql", "table": payload["table"], "file_name": payload["file_name"]},
            "form": {"file_name": payload["file_name"]},
            "profile": {
                "items": [
                    {
                        "name": "А",
                        "title": "",
                        "path": "1.25.24.24.24",
                        "marker": "48",
                        "path_to_data": "А",
                    }
                ],
                "attributes": [
                    {
                        "name": "А",
                        "title": "А",
                        "path": "3.3",
                        "marker": "9",
                        "title_path": "3.3.4.2.1",
                    }
                ],
            },
        }

    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)

    result = adapter_server.metadata_form_write_target_resolve(
        {
            "base_id": "upo_test",
            "table": "ConfigCASSave",
            "file_name": "form-guid.0",
            "element": "А",
            "property": "Заголовок",
            "value": "Новое А",
        }
    )

    assert result["status"] == "ok"
    assert result["target"]["section"] == "items"
    assert result["effective_target"]["section"] == "attributes"
    assert result["write_target"]["path"] == "3.3.4.2.1"
    assert result["effective_source"]["kind"] == "data_path_form_attribute_title"


def test_form_write_target_resolve_not_found_omits_internal_selector_fields(monkeypatch: pytest.MonkeyPatch) -> None:
    def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "status": "ok",
            "source": {"kind": "live_sql", "table": payload["table"], "file_name": payload["file_name"]},
            "form": {"file_name": payload["file_name"]},
            "profile": {"items": [{"name": "ДругоеПоле", "path": "1.10", "marker": "48"}]},
        }

    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)

    result = adapter_server.metadata_form_write_target_resolve(
        {
            "base_id": "upo_test",
            "table": "ConfigCASSave",
            "file_name": "form-guid.0",
            "element": "Список",
            "property": "Заголовок",
            "value": "Новое имя",
        }
    )

    assert result["status"] == "not_found"
    assert result["query"] == {"element": "Список"}


def test_saved_state_semantic_verify_uses_property_registry_alias(monkeypatch: pytest.MonkeyPatch) -> None:
    def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "status": "ok",
            "source": {"kind": "live_sql", "table": payload["table"], "file_name": payload["file_name"]},
            "form": {"file_name": payload["file_name"]},
            "profile": {
                "items": [
                    {
                        "name": "КнопкаПровести",
                        "title": "Провести",
                        "path": "1.20",
                        "marker": "31",
                        "parameters": [{"index": 14, "presentation": "ПоложениеВКоманднойПанели", "value": "2"}],
                    }
                ],
            },
        }

    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)

    result = adapter_server.semantic_verify_saved_state_apply(
        base_id="upo_test",
        table="ConfigCASSave",
        file_name="form-guid.0",
        proposal={
            "method": "metadata.form.element.write",
            "element": {"section": "items", "name": "КнопкаПровести", "path": "1.20"},
            "form_element_edits": [{"property": "command_bar_location", "value": "2"}],
        },
        timeout_seconds=30,
    )

    assert result is not None
    assert result["status"] == "ok"
    assert result["checks"] == [{"property": "command_bar_location", "expected": "2", "actual": "2", "ok": True}]


def test_form_write_target_resolve_explains_linked_command_source(monkeypatch: pytest.MonkeyPatch) -> None:
    def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "status": "ok",
            "source": {"kind": "live_sql", "table": payload["table"], "file_name": payload["file_name"]},
            "form": {"file_name": payload["file_name"]},
            "profile": {
                "items": [{"name": "КомандаПример1", "title": "", "path": "1.10", "title_path": "1.10.6"}],
                "commands": [{"name": "КомандаПример1", "title": "Пример1", "path": "5.3", "title_path": "5.3.3.2.1"}],
            },
        }

    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)

    result = adapter_server.metadata_form_write_target_resolve(
        {
            "base_id": "upo_test",
            "table": "ConfigCASSave",
            "file_name": "form-guid.0",
            "element_path": "1.10",
            "property": "Заголовок",
            "value": "ПРОВЕРКА",
        }
    )

    assert result["status"] == "ok"
    assert result["display"]["source_kind"] == "linked_command_title"
    assert result["display"]["actual"] == "Пример1"
    assert result["write_target"]["section"] == "commands"
    assert result["write_target"]["path"] == "5.3.3.2.1"
    assert result["alternatives"][0]["kind"] == "local_override_title"


def test_form_element_write_routes_data_path_title_to_form_attribute(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "status": "ok",
            "source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": "form-guid.0"},
            "form": {"guid": "form-guid", "file_name": "form-guid.0"},
            "profile": {
                "items": [
                    {
                        "name": "ПолеКонтрагент",
                        "title": "",
                        "path": "1.10",
                        "title_path": "1.10.6",
                        "path_to_data": "Контрагент",
                    }
                ],
                "attributes": [
                    {
                        "name": "Контрагент",
                        "title": "Контрагент",
                        "path": "3.2",
                        "title_path": "3.2.3.2.1",
                    }
                ],
            },
        }

    def fake_changes(payload: dict[str, Any]) -> dict[str, Any]:
        seen["changes"] = payload
        return {"schema": "onec_change_proposal.v1", "status": "accepted_for_review", "source": payload["source"], "edits": payload["edits"]}

    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
    monkeypatch.setattr(adapter_server, "changes_propose", fake_changes)

    result = adapter_server.metadata_form_element_write(
        {
            "base_id": "upo_test",
            "table": "ConfigCASSave",
            "file_name": "form-guid.0",
            "element": "ПолеКонтрагент",
            "allow_saved_state_write": True,
            "edits": [{"property": "Заголовок", "value": "Клиент"}],
        }
    )

    assert result["status"] == "accepted_for_review"
    assert seen["changes"]["edits"] == [
        {"path": "3.2.3.2.1", "value": "Клиент", "node_type": "auto", "property": "Заголовок", "old": "Контрагент"}
    ]
    assert result["requested_element"]["section"] == "items"
    assert result["element"]["section"] == "attributes"
    assert result["effective_sources"][0]["kind"] == "data_path_form_attribute_title"


def test_form_element_write_routes_tabular_data_path_title_to_form_attribute_field(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "status": "ok",
            "source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": "form-guid.0"},
            "form": {"guid": "form-guid", "file_name": "form-guid.0"},
            "profile": {
                "items": [
                    {
                        "name": "ТЗК1",
                        "title": "",
                        "path": "1.10",
                        "title_path": None,
                        "path_to_data": "ТЗ.К1",
                    }
                ],
                "attributes": [
                    {
                        "name": "ТЗ",
                        "title": "ТЗ",
                        "path": "3.6",
                        "title_path": "3.6.4.2.1",
                        "dynamic_list_fields": [
                            {
                                "name": "К1",
                                "data_name": "К1",
                                "id": "1",
                                "title": "К1",
                                "path": "3.6.14",
                                "title_path": "3.6.14.4.2.1",
                                "path_to_data": "ТЗ.К1",
                            }
                        ],
                    }
                ],
            },
        }

    def fake_changes(payload: dict[str, Any]) -> dict[str, Any]:
        seen["changes"] = payload
        return {"schema": "onec_change_proposal.v1", "status": "accepted_for_review", "source": payload["source"], "edits": payload["edits"]}

    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
    monkeypatch.setattr(adapter_server, "changes_propose", fake_changes)

    result = adapter_server.metadata_form_element_write(
        {
            "base_id": "upo_test",
            "table": "ConfigCASSave",
            "file_name": "form-guid.0",
            "element": "ТЗК1",
            "allow_saved_state_write": True,
            "edits": [{"property": "Заголовок", "value": "Колонка 1"}],
        }
    )

    assert result["status"] == "accepted_for_review"
    assert seen["changes"]["edits"] == [
        {"path": "3.6.14.4.2.1", "value": "Колонка 1", "node_type": "auto", "property": "Заголовок", "old": "К1"}
    ]
    assert result["element"]["section"] == "attribute_fields"
    assert result["effective_sources"][0]["kind"] == "data_path_form_attribute_field_title"


def test_form_element_write_routes_object_data_path_title_to_local_element(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "status": "ok",
            "source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": "form-guid.0"},
            "form": {"guid": "form-guid", "file_name": "form-guid.0"},
            "profile": {
                "items": [
                    {
                        "name": "ПолеКонтрагент",
                        "title": "",
                        "path": "1.10",
                        "title_path": "1.10.6",
                        "path_to_data": "Объект.Контрагент",
                    }
                ],
                "attributes": [{"name": "Объект", "title": "Объект", "path": "3.1", "title_path": "3.1.3.2.1"}],
            },
        }

    def fake_changes(payload: dict[str, Any]) -> dict[str, Any]:
        seen["changes"] = payload
        return {"schema": "onec_change_proposal.v1", "status": "accepted_for_review", "source": payload["source"], "edits": payload["edits"]}

    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
    monkeypatch.setattr(adapter_server, "changes_propose", fake_changes)

    result = adapter_server.metadata_form_element_write(
        {
            "base_id": "upo_test",
            "table": "ConfigCASSave",
            "file_name": "form-guid.0",
            "element": "ПолеКонтрагент",
            "allow_saved_state_write": True,
            "edits": [{"property": "Заголовок", "value": "Клиент"}],
        }
    )

    assert result["status"] == "accepted_for_review"
    assert seen["changes"]["edits"] == [
        {"path": "1.10.6", "value": "Клиент", "node_type": "auto", "property": "Заголовок", "old": ""}
    ]
    assert result["element"]["section"] == "items"
    assert result["effective_sources"][0]["kind"] == "data_path_object_attribute_local_title"


def test_form_element_write_allows_explicit_local_title_override(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "status": "ok",
            "source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": "form-guid.0"},
            "form": {"guid": "form-guid", "file_name": "form-guid.0"},
            "profile": {
                "items": [
                    {
                        "name": "ПолеКонтрагент",
                        "title": "",
                        "path": "1.10",
                        "title_path": "1.10.6",
                        "path_to_data": "Объект.Контрагент",
                    }
                ],
            },
        }

    def fake_changes(payload: dict[str, Any]) -> dict[str, Any]:
        seen["changes"] = payload
        return {"schema": "onec_change_proposal.v1", "status": "accepted_for_review", "source": payload["source"], "edits": payload["edits"]}

    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
    monkeypatch.setattr(adapter_server, "changes_propose", fake_changes)

    result = adapter_server.metadata_form_element_write(
        {
            "base_id": "upo_test",
            "table": "ConfigCASSave",
            "file_name": "form-guid.0",
            "element": "ПолеКонтрагент",
            "allow_saved_state_write": True,
            "edits": [{"property": "Заголовок", "value": "Клиент", "source": "local_override"}],
        }
    )

    assert result["status"] == "accepted_for_review"
    assert seen["changes"]["edits"] == [
        {"path": "1.10.6", "value": "Клиент", "node_type": "auto", "property": "Заголовок", "old": ""}
    ]
    assert result["effective_sources"][0]["kind"] == "local_override_title"


def test_metadata_write_routes_form_plan(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
        seen["payload"] = payload
        return {"schema": "onec_form_element_write_apply.v1", "status": "planned", "proposal": {"status": "accepted_for_review"}}

    monkeypatch.setattr(adapter_server, "metadata_form_element_write_apply", fake_apply)

    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "target": {"kind": "form", "table": "ConfigCASSave", "file_name": "form-guid.0", "element": "КомандаПример1"},
            "mode": "plan",
            "edits": [{"property": "Заголовок", "value": "ПРОВЕРКА"}],
        }
    )

    assert seen["payload"]["table"] == "ConfigCASSave"
    assert seen["payload"]["file_name"] == "form-guid.0"
    assert seen["payload"]["element"] == "КомандаПример1"
    assert seen["payload"]["allow_saved_state_write"] is True
    assert "target" not in seen["payload"]
    assert "kind" not in seen["payload"]
    assert result["schema"] == "onec_metadata_write.v1"
    assert result["routed_method"] == "metadata.form.element.write_apply"


def test_metadata_write_routes_common_form_command_path_to_command_button_writer(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_command_button_write(payload: dict[str, Any]) -> dict[str, Any]:
        seen["payload"] = payload
        return {
            "schema": "onec_form_command_button_write.v1",
            "status": "planned",
            "idempotency": {"status": "new"},
            "semantic_verify": {"verified": True},
        }

    monkeypatch.setattr(adapter_server, "metadata_form_command_button_write", fake_command_button_write)
    monkeypatch.setattr(adapter_server, "resolve_write_gate_context", lambda payload: payload)

    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "extension": "test2",
            "target": {"canonical_path": "ОбщаяФорма.t_Форма.Команда.РасчетС"},
            "mode": "plan",
            "title": "РасчетС",
        }
    )

    assert result["status"] == "planned"
    assert result["routed_method"] == "metadata.form.command_button.write"
    assert seen["payload"]["form"] == "t_Форма"
    assert seen["payload"]["command_name"] == "РасчетС"
    assert seen["payload"]["button_name"] == "РасчетС"
    assert seen["payload"]["allow_saved_state_write"] is True
    assert result["path_resolution"]["path_kind"] == "form_command"


def test_metadata_write_routes_common_form_button_path_to_command_button_writer(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_command_button_write(payload: dict[str, Any]) -> dict[str, Any]:
        seen["payload"] = payload
        return {"schema": "onec_form_command_button_write.v1", "status": "planned"}

    monkeypatch.setattr(adapter_server, "metadata_form_command_button_write", fake_command_button_write)
    monkeypatch.setattr(adapter_server, "resolve_write_gate_context", lambda payload: payload)

    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "extension": "test2",
            "target": {"canonical_path": "ОбщаяФорма.t_Форма.Кнопка.РасчетС"},
            "mode": "plan",
        }
    )

    assert result["status"] == "planned"
    assert result["routed_method"] == "metadata.form.command_button.write"
    assert seen["payload"]["command_name"] == "РасчетС"
    assert result["path_resolution"]["form_member_kind"] == "button"


def test_metadata_write_history_records_call_method_write(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"server": "sql.example", "database": base_id}, None))

    def fake_command_button_write(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "schema": "onec_form_command_button_write.v1",
            "status": "verified",
            "base_id": payload["base_id"],
            "backup": {"backup_id": "0123456789abcdef0123456789abcdef"},
            "target": {"kind": "CommonForm", "form": "t_Форма", "extension": "test2", "table": "ConfigCASSave"},
        }

    monkeypatch.setattr(adapter_server, "metadata_form_command_button_write", fake_command_button_write)

    write_result = adapter_server.call_method(
        "metadata.form.command_button.write",
        {
            "base_id": "upo_test",
            "extension": "test2",
            "form": "t_Форма",
            "command_name": "РасчетС",
            "allow_saved_state_write": True,
        },
    )

    assert write_result["operation_id"]
    history = adapter_server.call_method("metadata.write.history", {"base_id": "upo_test", "operation_id": write_result["operation_id"]})
    assert history["status"] == "ok"
    assert history["counts"]["operations"] == 1
    operation = history["operations"][0]
    assert operation["operation_id"] == write_result["operation_id"]
    assert operation["method"] == "metadata.form.command_button.write"
    assert operation["status"] == "verified"
    assert operation["backup_ids"] == ["0123456789abcdef0123456789abcdef"]
    assert operation["result"]["status"] == "verified"

    by_method = adapter_server.call_method("metadata.write.history", {"base_id": "upo_test", "operation_method": "metadata.form.command_button.write"})
    assert by_method["counts"]["operations"] == 1
    assert by_method["operations"][0]["operation_id"] == write_result["operation_id"]

    by_backup = adapter_server.call_method("metadata.write.history", {"base_id": "upo_test", "backup_id": "0123456789abcdef0123456789abcdef"})
    assert by_backup["counts"]["operations"] == 1
    assert by_backup["operations"][0]["operation_id"] == write_result["operation_id"]

    summary = adapter_server.call_method("metadata.write.history", {"base_id": "upo_test", "include_summary": True})
    assert summary["summary"]["by_method"]["metadata.form.command_button.write"] == 1
    assert summary["summary"]["by_status"]["verified"] == 1
    assert summary["summary"]["with_backups"] == 1


def test_metadata_write_rollback_uses_operation_backup(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"server": "sql.example", "database": base_id}, None))
    seen: dict[str, Any] = {}

    monkeypatch.setattr(
        adapter_server,
        "metadata_form_command_button_write",
        lambda payload: {
            "schema": "onec_form_command_button_write.v1",
            "status": "verified",
            "base_id": payload["base_id"],
            "backup": {"backup_id": "0123456789abcdef0123456789abcdef"},
        },
    )
    def fake_storage_saved_state_rollback(payload: dict[str, Any]) -> dict[str, Any]:
        seen["rollback_payload"] = payload
        return {
            "schema": "onec_storage_saved_state_rollback.v1",
            "status": "applied",
            "applied": True,
            "base_id": payload["base_id"],
            "backup": {"backup_id": payload["backup_id"]},
        }

    monkeypatch.setattr(adapter_server, "storage_saved_state_rollback", fake_storage_saved_state_rollback)

    write_result = adapter_server.call_method(
        "metadata.form.command_button.write",
        {
            "base_id": "upo_test",
            "form": "t_Форма",
            "command_name": "РасчетС",
            "allow_saved_state_write": True,
        },
    )
    rollback = adapter_server.call_method(
        "metadata.write.rollback",
        {
            "base_id": "upo_test",
            "operation_id": write_result["operation_id"],
            "allow_sql_saved_state_rollback": True,
        },
    )

    assert rollback["status"] == "applied"
    assert rollback["applied"] is True
    assert rollback["backup_id"] == "0123456789abcdef0123456789abcdef"
    assert seen["rollback_payload"]["backup_id"] == "0123456789abcdef0123456789abcdef"
    assert seen["rollback_payload"]["allow_sql_saved_state_rollback"] is True


def test_metadata_write_rollback_checks_explicit_gate_before_repository(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "repository_apply_gate",
        lambda *_args, **_kwargs: pytest.fail("repository gate must not run before rollback opt-in"),
    )

    result = adapter_server.metadata_write_rollback(
        {
            "base_id": "upo_test",
            "operation_id": "operation-id",
        }
    )

    assert result["status"] == "invalid_argument"
    assert result["argument"] == "allow_sql_saved_state_rollback"


def test_metadata_write_active_form_target_requires_saved_state_prepare(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_prepare(payload: dict[str, Any]) -> dict[str, Any]:
        seen["prepare"] = payload
        return {"schema": "onec_saved_state_prepare.v1", "status": "plan_ready", "ready_to_copy": True}

    monkeypatch.setattr(adapter_server, "metadata_saved_state_prepare", fake_prepare)

    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "target": {"kind": "form", "table": "ConfigCAS", "file_name": "form-guid.0", "element": "КомандаПример1"},
            "mode": "plan",
            "edits": [{"property": "Заголовок", "value": "ПРОВЕРКА"}],
        }
    )

    assert result["status"] == "blocked"
    assert result["error"] == "saved_state_prepare_required"
    assert result["prepared_target"] == {"layer": "extension_saved_state"}
    assert result["next_resolution"]["method"] == "metadata.saved_state.prepare"
    assert result["next_resolution"]["params"] == {
        "base_id": "upo_test",
        "layer": "extension_saved_state",
        "mode": "plan",
        "selector_required": "Pass a public 1C ref or kind/name selector.",
    }
    assert seen["prepare"]["mode"] == "plan"
    assert seen["prepare"]["source_table"] == "ConfigCAS"
    assert seen["prepare"]["target_table"] == "ConfigCASSave"


def test_metadata_write_active_form_target_auto_prepares_when_apply_allowed(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_prepare(payload: dict[str, Any]) -> dict[str, Any]:
        seen["prepare"] = payload
        return {"schema": "onec_saved_state_prepare.v1", "status": "verified", "applied": True}

    def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
        seen["apply"] = payload
        return {"schema": "onec_form_element_write_apply.v1", "status": "verified", "applied": True}

    monkeypatch.setattr(adapter_server, "metadata_saved_state_prepare", fake_prepare)
    monkeypatch.setattr(adapter_server, "metadata_form_element_write_apply", fake_apply)

    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "target": {"kind": "form", "table": "ConfigCAS", "file_name": "form-guid.0", "element": "КомандаПример1"},
            "mode": "apply_and_verify",
            "allow_sql_saved_state_apply": True,
            "edits": [{"property": "Заголовок", "value": "ПРОВЕРКА"}],
        }
    )

    assert seen["prepare"]["mode"] == "apply_and_verify"
    assert seen["prepare"]["allow_sql_saved_state_prepare"] is True
    assert seen["apply"]["table"] == "ConfigCASSave"
    assert seen["apply"]["file_name"] == "form-guid.0"
    assert result["status"] == "verified"
    assert result["resolution"]["method"] == "metadata.saved_state.prepare"


def test_metadata_write_missing_form_target_auto_prepares_and_retries(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: dict[str, int] = {"apply": 0}
    seen: dict[str, Any] = {}
    monkeypatch.setattr(
        adapter_server,
        "extension_filter_to_guid",
        lambda base_id, extension, method: (
            "fb26cf42-7609-11f1-828f-005056b0d483",
            None,
        ),
    )

    def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
        calls["apply"] += 1
        seen["apply"] = payload
        if calls["apply"] == 1:
            return {"schema": "onec_form_element_write_apply.v1", "status": "not_found", "proposal": {"status": "not_found"}}
        return {"schema": "onec_form_element_write_apply.v1", "status": "verified", "applied": True}

    def fake_prepare(payload: dict[str, Any]) -> dict[str, Any]:
        seen["prepare"] = payload
        return {"schema": "onec_saved_state_prepare.v1", "status": "verified", "applied": True}

    monkeypatch.setattr(adapter_server, "metadata_form_element_write_apply", fake_apply)
    monkeypatch.setattr(adapter_server, "metadata_saved_state_prepare", fake_prepare)

    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "target": {"kind": "form", "extension": "test2", "object_type": "CommonForm", "object_name": "t_Форма", "element": "КомандаПример1"},
            "mode": "apply_and_verify",
            "allow_sql_saved_state_apply": True,
            "edits": [{"property": "Заголовок", "value": "ПРОВЕРКА"}],
        }
    )

    assert calls["apply"] == 2
    assert seen["apply"]["table"] == "ConfigCASSave"
    assert seen["prepare"]["target_table"] == "ConfigCASSave"
    assert seen["prepare"]["extension"] == "test2"
    assert seen["prepare"]["object_type"] == "CommonForm"
    assert seen["prepare"]["object_name"] == "t_Форма"
    assert result["status"] == "verified"


def test_metadata_write_plan_requires_full_path() -> None:
    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {"canonical_path": "Наименование"},
            "intent": {"operation": "property_change", "property": "Синоним", "value": "Имя"},
        }
    )

    assert result["schema"] == "onec_metadata_write_plan.v1"
    assert result["allowed"] is False
    assert result["status"] == "needs_origin"
    assert {problem["code"] for problem in result["problems"]} >= {"local_or_short_name"}


def test_metadata_write_plan_blocks_effective_path_without_origin() -> None:
    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "resolve_origin": False,
            "target": {"canonical_path": "Справочник.Контрагенты.Наименование"},
            "intent": {"operation": "property_change", "property": "Синоним", "value": "Контрагент"},
        }
    )

    assert result["allowed"] is False
    assert result["status"] == "needs_origin"
    assert result["target"]["canonical_path"] == "Справочник.Контрагенты.Наименование"
    assert result["path_resolution"]["kind"] == "Catalog"
    assert "layer_provenance" in result["required_guards"]
    assert any(problem["code"] == "origin_lookup_required" for problem in result["problems"])


def test_metadata_write_plan_includes_origin_lookup(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_definition_find(payload: dict[str, Any]) -> dict[str, Any]:
        seen.update(payload)
        return {
            "schema": "onec_metadata_definition_find.v1",
            "status": "ok",
            "object": {"kind": "Catalog", "kind_ru": "Справочник", "name": "Контрагенты", "guid": "catalog-guid"},
            "matches": [
                {
                    "area": "object",
                    "kind": "Реквизит",
                    "name": "Наименование",
                    "match_by": "name_exact",
                    "location": {"presentation": "Справочник.Контрагенты.Наименование"},
                    "origin": {"source": "configuration", "presentation": "Конфигурация", "status": "ok"},
                    "read_selector": {"method": "metadata.object.attributes", "base_id": "upo_test", "kind": "Catalog", "name": "Контрагенты"},
                }
            ],
            "related_selectors": {"full": {"method": "metadata.object.full", "base_id": "upo_test", "kind": "Catalog", "name": "Контрагенты"}},
            "counts": {"matches": 1, "origin_extension": 0, "origin_unresolved": 0},
        }

    monkeypatch.setattr(adapter_server, "metadata_definition_find", fake_definition_find)

    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {"canonical_path": "Справочник.Контрагенты.Наименование"},
            "intent": {"operation": "property_change", "property": "Синоним", "value": "Контрагент"},
        }
    )

    assert seen["query"] == "Наименование"
    assert seen["kind"] == "Catalog"
    assert seen["name"] == "Контрагенты"
    assert seen["areas"] == ["object", "extensions"]
    assert result["allowed"] is False
    assert result["status"] == "needs_route"
    assert result["origin_lookup"]["status"] == "ok"
    assert result["origin_lookup"]["matches"][0]["origin"]["source"] == "configuration"
    assert result["origin_lookup"]["matches"][0]["read_selector"]["method"] == "metadata.object.attributes"
    assert result["route"]["recommended_write"]["write_surface"] == "base_saved_state"
    assert result["route"]["recommended_write"]["table"] == "ConfigSave"
    assert any(problem["code"] == "write_route_required" for problem in result["problems"])


def test_metadata_write_plan_recommends_extension_saved_state(monkeypatch: pytest.MonkeyPatch) -> None:
    def fake_definition_find(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "schema": "onec_metadata_definition_find.v1",
            "status": "ok",
            "object": {"kind": "Catalog", "kind_ru": "Справочник", "name": "Контрагенты", "guid": "catalog-guid"},
            "matches": [
                {
                    "area": "object",
                    "kind": "Реквизит",
                    "name": "ВнешнийКод",
                    "match_by": "name_exact",
                    "origin": {
                        "source": "extension",
                        "presentation": "Расширение",
                        "status": "ok",
                        "extension": {"name": "CRM"},
                    },
                    "read_selector": {"method": "metadata.object.attributes", "base_id": "upo_test", "kind": "Catalog", "name": "Контрагенты"},
                }
            ],
            "counts": {"matches": 1, "origin_extension": 1, "origin_unresolved": 0},
        }

    monkeypatch.setattr(adapter_server, "metadata_definition_find", fake_definition_find)

    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {"canonical_path": "Справочник.Контрагенты.ВнешнийКод"},
            "intent": {"operation": "property_change", "property": "Синоним", "value": "Внешний код"},
        }
    )

    recommended = result["route"]["recommended_write"]
    assert recommended["write_surface"] == "extension_saved_state"
    assert recommended["table"] == "ConfigCASSave"
    assert recommended["extension"]["name_or_guid"] == "CRM"
    assert result["status"] == "needs_route"


def test_metadata_write_plan_blocks_base_preference_for_extension_origin(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_definition_find",
        lambda payload: {
            "schema": "onec_metadata_definition_find.v1",
            "status": "ok",
            "object": {"kind": "Catalog", "kind_ru": "Справочник", "name": "Контрагенты", "guid": "catalog-guid"},
            "matches": [
                {
                    "area": "object",
                    "kind": "Реквизит",
                    "name": "ВнешнийКод",
                    "match_by": "name_exact",
                    "origin": {
                        "source": "extension",
                        "presentation": "Расширение",
                        "status": "ok",
                        "extension": {"name": "CRM"},
                    },
                }
            ],
            "counts": {"matches": 1, "origin_extension": 1, "origin_unresolved": 0},
        },
    )

    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "preferred_layer": "base",
            "target": {"canonical_path": "Справочник.Контрагенты.ВнешнийКод"},
            "intent": {"operation": "property_change", "property": "Синоним", "value": "Внешний код"},
        }
    )

    assert result["route"]["preferred_layer"] == "base"
    assert result["route"]["recommended_write"]["write_surface"] == "extension_saved_state"
    conflict = [problem for problem in result["problems"] if problem["code"] == "preferred_layer_conflict"][0]
    assert conflict["preferred_layer"] == "base"
    assert conflict["recommended_layer"] == "extension"


def test_metadata_write_plan_accepts_matching_preferred_extension(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_definition_find",
        lambda payload: {
            "schema": "onec_metadata_definition_find.v1",
            "status": "ok",
            "object": {"kind": "Catalog", "kind_ru": "Справочник", "name": "Контрагенты", "guid": "catalog-guid"},
            "matches": [
                {
                    "area": "object",
                    "kind": "Реквизит",
                    "name": "ВнешнийКод",
                    "match_by": "name_exact",
                    "origin": {
                        "source": "extension",
                        "presentation": "Расширение",
                        "status": "ok",
                        "extension": {"name": "CRM"},
                    },
                }
            ],
            "counts": {"matches": 1, "origin_extension": 1, "origin_unresolved": 0},
        },
    )

    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "preferred_layer": "extension",
            "preferred_extension": "crm",
            "target": {"canonical_path": "Справочник.Контрагенты.ВнешнийКод"},
            "intent": {"operation": "property_change", "property": "Синоним", "value": "Внешний код"},
        }
    )

    assert result["route"]["preferred_extension"] == "crm"
    assert result["route"]["recommended_write"]["extension"]["name_or_guid"] == "CRM"
    assert not any(problem["code"] == "preferred_extension_conflict" for problem in result["problems"])


def test_metadata_write_plan_blocks_wrong_preferred_extension(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_definition_find",
        lambda payload: {
            "schema": "onec_metadata_definition_find.v1",
            "status": "ok",
            "object": {"kind": "Catalog", "kind_ru": "Справочник", "name": "Контрагенты", "guid": "catalog-guid"},
            "matches": [
                {
                    "area": "object",
                    "kind": "Реквизит",
                    "name": "ВнешнийКод",
                    "match_by": "name_exact",
                    "origin": {
                        "source": "extension",
                        "presentation": "Расширение",
                        "status": "ok",
                        "extension": {"name": "CRM"},
                    },
                }
            ],
            "counts": {"matches": 1, "origin_extension": 1, "origin_unresolved": 0},
        },
    )

    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "preferred_layer": "extension",
            "preferred_extension": "Обмен",
            "target": {"canonical_path": "Справочник.Контрагенты.ВнешнийКод"},
            "intent": {"operation": "property_change", "property": "Синоним", "value": "Внешний код"},
        }
    )

    conflict = [problem for problem in result["problems"] if problem["code"] == "preferred_extension_conflict"][0]
    assert conflict["preferred_extension"] == "Обмен"
    assert conflict["recommended_extension"] == "CRM"


def test_metadata_write_plan_blocks_extension_preference_for_base_origin(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_definition_find",
        lambda payload: {
            "schema": "onec_metadata_definition_find.v1",
            "status": "ok",
            "object": {"kind": "Catalog", "kind_ru": "Справочник", "name": "Контрагенты", "guid": "catalog-guid"},
            "matches": [
                {
                    "area": "object",
                    "kind": "Реквизит",
                    "name": "Наименование",
                    "match_by": "name_exact",
                    "origin": {"source": "configuration", "presentation": "Конфигурация", "status": "ok"},
                }
            ],
            "counts": {"matches": 1, "origin_extension": 0, "origin_unresolved": 0},
        },
    )

    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "preferred_layer": "расширение",
            "target": {"canonical_path": "Справочник.Контрагенты.Наименование"},
            "intent": {"operation": "property_change", "property": "Синоним", "value": "Контрагент"},
        }
    )

    assert result["route"]["preferred_layer"] == "extension"
    assert result["route"]["recommended_write"]["write_surface"] == "base_saved_state"
    conflict = [problem for problem in result["problems"] if problem["code"] == "preferred_layer_conflict"][0]
    assert conflict["preferred_layer"] == "extension"
    assert conflict["recommended_layer"] == "base"


def test_metadata_write_plan_blocks_conflicting_origin_layers(monkeypatch: pytest.MonkeyPatch) -> None:
    def fake_definition_find(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "schema": "onec_metadata_definition_find.v1",
            "status": "ok",
            "object": {"kind": "Catalog", "kind_ru": "Справочник", "name": "Контрагенты", "guid": "catalog-guid"},
            "matches": [
                {
                    "area": "object",
                    "kind": "Реквизит",
                    "name": "Наименование",
                    "match_by": "name_exact",
                    "origin": {"source": "configuration", "presentation": "Конфигурация", "status": "ok"},
                },
                {
                    "area": "extensions",
                    "kind": "Реквизит",
                    "name": "Наименование",
                    "match_by": "name_exact",
                    "origin": {
                        "source": "extension",
                        "presentation": "Расширение",
                        "status": "ok",
                        "extension": {"name": "CRM"},
                    },
                },
            ],
            "counts": {"matches": 2, "origin_extension": 1, "origin_unresolved": 0},
        }

    monkeypatch.setattr(adapter_server, "metadata_definition_find", fake_definition_find)

    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {"canonical_path": "Справочник.Контрагенты.Наименование"},
            "intent": {"operation": "property_change", "property": "Синоним", "value": "Контрагент"},
        }
    )

    recommended = result["route"]["recommended_write"]
    assert recommended["write_surface"] == "blocked_conflict"
    assert recommended["status"] == "blocked"
    assert "configuration" in recommended["layers"]
    assert "extension:CRM" in recommended["layers"]
    assert any(problem["code"] == "blocked_conflict" for problem in result["problems"])


def test_metadata_write_plan_uses_provided_configuration_origin() -> None:
    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {
                "canonical_path": "ОбщийМодуль.Интеграция.Отправить",
                "origin": {"source": "configuration", "presentation": "Конфигурация", "status": "ok"},
            },
            "intent": {"operation": "replace", "old": "a", "new": "b"},
        }
    )

    recommended = result["route"]["recommended_write"]
    assert result["allowed"] is False
    assert result["origin_lookup"]["method"] == "provided_origin_evidence"
    assert recommended["write_surface"] == "base_saved_state"
    assert any(problem["code"] == "write_route_required" for problem in result["problems"])


def test_metadata_write_plan_uses_provided_extension_origin() -> None:
    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {
                "canonical_path": "ОбщийМодуль.Интеграция.Отправить",
                "origin": {
                    "source": "extension",
                    "presentation": "Расширение",
                    "status": "ok",
                    "extension": {"name": "РасширениеCRM"},
                },
            },
            "intent": {"operation": "replace", "old": "a", "new": "b"},
        }
    )

    recommended = result["route"]["recommended_write"]
    assert result["origin_lookup"]["method"] == "provided_origin_evidence"
    assert recommended["write_surface"] == "extension_saved_state"
    assert recommended["extension"]["name_or_guid"] == "РасширениеCRM"


def test_metadata_write_plan_blocks_provided_unresolved_cas_origin() -> None:
    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {
                "canonical_path": "ОбщийМодуль.Интеграция.Отправить",
                "origin": {
                    "source": "cas_reference",
                    "presentation": "CAS module reference",
                    "status": "owner_unresolved",
                    "write_surface": "requires_owner_resolution",
                },
            },
            "intent": {"operation": "replace", "old": "a", "new": "b"},
        }
    )

    recommended = result["route"]["recommended_write"]
    assert result["allowed"] is False
    assert recommended["write_surface"] == "blocked_unknown"
    assert recommended["reason"] == "origin_layer_not_resolved"
    assert any(problem["code"] == "blocked_unknown" for problem in result["problems"])


def test_metadata_write_plan_reports_ambiguous_same_layer_origin(monkeypatch: pytest.MonkeyPatch) -> None:
    def fake_definition_find(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "schema": "onec_metadata_definition_find.v1",
            "status": "ok",
            "object": {"kind": "Catalog", "kind_ru": "Справочник", "name": "Контрагенты", "guid": "catalog-guid"},
            "matches": [
                {
                    "area": "object",
                    "kind": "Реквизит",
                    "name": "Код",
                    "match_by": "name_exact",
                    "location": {"presentation": "Справочник.Контрагенты.Код"},
                    "origin": {"source": "configuration", "presentation": "Конфигурация", "status": "ok"},
                },
                {
                    "area": "form",
                    "kind": "РеквизитФормы",
                    "name": "Код",
                    "match_by": "name_exact",
                    "location": {"presentation": "Справочник.Контрагенты.Форма.ФормаЭлемента.Код"},
                    "origin": {"source": "configuration", "presentation": "Конфигурация", "status": "ok"},
                },
            ],
            "counts": {"matches": 2, "origin_extension": 0, "origin_unresolved": 0},
        }

    monkeypatch.setattr(adapter_server, "metadata_definition_find", fake_definition_find)

    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {"canonical_path": "Справочник.Контрагенты.Код"},
            "intent": {"operation": "property_change", "property": "Синоним", "value": "Код"},
        }
    )

    assert result["route"]["recommended_write"]["write_surface"] == "base_saved_state"
    ambiguity = [problem for problem in result["problems"] if problem["code"] == "ambiguous_origin_matches"][0]
    assert ambiguity["match_count"] == 2
    assert ambiguity["candidates"][0]["presentation"] == "Справочник.Контрагенты.Код"
    assert ambiguity["candidates"][1]["area"] == "form"


def test_metadata_write_plan_infers_form_target_from_canonical_path(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_definition_find(payload: dict[str, Any]) -> dict[str, Any]:
        seen.update(payload)
        return {
            "schema": "onec_metadata_definition_find.v1",
            "status": "ok",
            "object": {"kind": "Document", "kind_ru": "Документ", "name": "Заказ", "guid": "doc-guid"},
            "matches": [
                {
                    "area": "form",
                    "kind": "ЭлементФормы",
                    "name": "КнопкаЗаписать",
                    "match_by": "name_exact",
                    "origin": {"source": "configuration", "presentation": "Конфигурация", "status": "ok"},
                }
            ],
            "counts": {"matches": 1, "origin_extension": 0, "origin_unresolved": 0},
        }

    monkeypatch.setattr(adapter_server, "metadata_definition_find", fake_definition_find)

    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {"canonical_path": "Документ.Заказ.Форма.ФормаДокумента.КнопкаЗаписать"},
            "intent": {"operation": "property_change", "property": "Заголовок", "value": "Записать"},
        }
    )

    assert result["target"]["target_kind"] == "form"
    assert result["path_resolution"]["path_kind"] == "form_member"
    assert result["path_resolution"]["form_name"] == "ФормаДокумента"
    assert seen["query"] == "КнопкаЗаписать"
    assert seen["areas"] == ["form", "extensions"]
    assert result["route"]["apply_method"] == "metadata.form.element.write_apply"
    hint_payload = result["route"]["apply_payload_hint"]["payload"]
    assert hint_payload["kind"] == "Document"
    assert hint_payload["name"] == "Заказ"
    assert hint_payload["form"] == "ФормаДокумента"
    assert hint_payload["element"] == "КнопкаЗаписать"
    assert result["route"]["apply_payload_hint"]["ready_for_apply_method"] is False
    next_resolution = result["route"]["apply_payload_hint"]["next_resolution"]
    assert next_resolution["method"] == "metadata.form.write_target.resolve"
    assert next_resolution["params"] == {
        "base_id": "upo_test",
        "ref": "Document.Заказ",
        "kind": "Document",
        "name": "Заказ",
        "form": "ФормаДокумента",
        "element": "КнопкаЗаписать",
    }
    assert "payload" not in next_resolution


def test_metadata_write_plan_infers_module_target_from_common_module_path(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_definition_find(payload: dict[str, Any]) -> dict[str, Any]:
        seen.update(payload)
        return {
            "schema": "onec_metadata_definition_find.v1",
            "status": "ok",
            "object": {"kind": "CommonModule", "kind_ru": "ОбщийМодуль", "name": "Интеграция", "guid": "module-guid"},
            "matches": [
                {
                    "area": "modules",
                    "kind": "Процедура",
                    "name": "Отправить",
                    "match_by": "name_exact",
                    "origin": {"source": "configuration", "presentation": "Конфигурация", "status": "ok"},
                }
            ],
            "counts": {"matches": 1, "origin_extension": 0, "origin_unresolved": 0},
        }

    monkeypatch.setattr(adapter_server, "metadata_definition_find", fake_definition_find)

    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {"canonical_path": "ОбщийМодуль.Интеграция.Отправить"},
            "intent": {"operation": "replace", "old": "Сообщить(\"old\");", "new": "Сообщить(\"new\");"},
        }
    )

    assert result["target"]["target_kind"] == "module"
    assert result["path_resolution"]["path_kind"] == "module_routine"
    assert result["path_resolution"]["routine_name"] == "Отправить"
    assert seen["query"] == "Отправить"
    assert seen["areas"] == ["modules", "extensions"]
    assert result["route"]["apply_method"] == "metadata.module.write_apply"
    hint_payload = result["route"]["apply_payload_hint"]["payload"]
    assert hint_payload["kind"] == "CommonModule"
    assert hint_payload["name"] == "Интеграция"
    assert hint_payload["routine_name"] == "Отправить"
    assert result["route"]["apply_payload_hint"]["ready_for_apply_method"] is False
    next_resolution = result["route"]["apply_payload_hint"]["next_resolution"]
    assert next_resolution["method"] == "metadata.saved_state.modules.search"
    assert next_resolution["params"] == {
        "base_id": "upo_test",
        "ref": "CommonModule.Интеграция",
        "kind": "CommonModule",
        "name": "Интеграция",
        "query": "Отправить",
    }
    assert "payload" not in next_resolution


def test_metadata_write_plan_infers_module_target_from_common_form_routine_path(monkeypatch: pytest.MonkeyPatch) -> None:
    def fake_definition_find(payload: dict[str, Any]) -> dict[str, Any]:
        raise AssertionError("resolve_origin=false must not call metadata.definition.find")

    monkeypatch.setattr(adapter_server, "metadata_definition_find", fake_definition_find)

    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "resolve_origin": False,
            "target": {"canonical_path": "ОбщаяФорма.t_Форма.ЗаменаДомена"},
            "intent": {"operation": "replace", "routine_text": "Процедура ЗаменаДомена(Команда)\nКонецПроцедуры\n"},
        }
    )

    assert result["target"]["target_kind"] == "module"
    assert result["path_resolution"]["path_kind"] == "module_routine"
    assert result["path_resolution"]["section"] == "form_module"
    assert result["path_resolution"]["kind"] == "CommonForm"
    assert result["path_resolution"]["name"] == "t_Форма"
    assert result["path_resolution"]["routine_name"] == "ЗаменаДомена"
    assert result["route"]["apply_method"] == "metadata.module.write_apply"
    hint_payload = result["route"]["apply_payload_hint"]["payload"]
    assert hint_payload["kind"] == "CommonForm"
    assert hint_payload["name"] == "t_Форма"
    assert hint_payload["routine_name"] == "ЗаменаДомена"
    assert result["route"]["apply_payload_hint"]["ready_for_apply_method"] is False
    next_resolution = result["route"]["apply_payload_hint"]["next_resolution"]
    assert next_resolution["method"] == "metadata.saved_state.modules.search"
    assert next_resolution["params"] == {
        "base_id": "upo_test",
        "ref": "CommonForm.t_Форма",
        "kind": "CommonForm",
        "name": "t_Форма",
        "query": "ЗаменаДомена",
    }


def test_metadata_write_plan_allows_concrete_module_saved_state_reference() -> None:
    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "module",
                "module_ref": "ConfigCASSave:object-guid__module-guid.0#stream:4",
            },
            "intent": {"operation": "replace", "old": "Сообщить(\"old\");", "new": "Сообщить(\"new\");"},
        }
    )

    assert result["allowed"] is True
    assert result["status"] == "planned"
    assert result["route"]["write_surface"] == "saved_state"
    assert result["route"]["apply_method"] == "metadata.module.write_apply"
    assert "expected_sha1" in result["required_guards"]
    assert result["target"]["concrete_reference_field"] == "module_ref"
    assert result["route"]["apply_payload_hint"]["ready_for_apply_method"] is True


def test_metadata_write_plan_resolves_public_module_selector_internally(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_search(payload: dict[str, Any]) -> dict[str, Any]:
        seen.update(payload)
        return {
            "schema": "onec_saved_state_module_search.v1",
            "status": "ok",
            "modules": [
                {
                    "owner": {"kind": "Catalog", "name": "Контрагенты"},
                    "module": {"kind": "manager_module", "name": "Модуль менеджера"},
                    "qualified_name": "Контрагенты.Модуль менеджера",
                    "payload": {"sha1": "saved-payload-sha1"},
                    "streams": [
                        {
                            "module_ref": "ConfigSave:object-guid.3#stream:0",
                            "module": {"kind": "manager_module", "name": "Модуль менеджера"},
                            "qualified_name": "Контрагенты.Модуль менеджера",
                        }
                    ],
                }
            ],
        }

    monkeypatch.setattr(adapter_server, "metadata_saved_state_modules_search", fake_search)

    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "module",
                "ref": "Catalog.Контрагенты",
                "module": "Модуль менеджера",
                "qualified_name": "Контрагенты.Модуль менеджера",
                "stream_ordinal": 1,
            },
            "intent": {
                "operation": "replace_with_control",
                "control_fragment": "Процедура Тест()",
                "new": "Процедура Тест()\nКонецПроцедуры",
            },
            "resolve_origin": False,
        }
    )

    assert seen["ref"] == "Catalog.Контрагенты"
    assert seen["include_storage"] is True
    assert result["allowed"] is True
    assert result["route"]["name_resolution"]["status"] == "resolved"
    assert result["route"]["name_resolution"]["selector"]["ref"] == "Catalog.Контрагенты"
    hint = result["route"]["apply_payload_hint"]
    assert hint["ready_for_apply_method"] is True
    assert hint["payload"]["module_ref"] == "ConfigSave:object-guid.3#stream:0"
    assert hint["payload"]["expected_sha1"] == "saved-payload-sha1"


def test_metadata_write_plan_normalizes_top_level_public_module_selector(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_search(payload: dict[str, Any]) -> dict[str, Any]:
        seen.update(payload)
        return {
            "schema": "onec_saved_state_module_search.v1",
            "status": "ok",
            "modules": [{
                "form": {"name": "ФормаОтчетаПродкорзина"},
                "module": {"kind": "form_module", "name": "Модуль формы"},
                "payload": {"sha1": "saved-payload-sha1"},
                "streams": [{
                    "module_ref": "ConfigCASSave:extension-guid__form-guid.0",
                    "form": {"name": "ФормаОтчетаПродкорзина"},
                    "module": {"kind": "form_module", "name": "Модуль формы"},
                }],
            }],
        }

    monkeypatch.setattr(adapter_server, "metadata_saved_state_modules_search", fake_search)

    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target_kind": "module",
            "ref": "Report.фс_ПродовольственнаяКорзина",
            "form": "ФормаОтчетаПродкорзина",
            "module": "Модуль формы",
            "extension": "фс_Отчеты1",
            "intent": {
                "operation": "replace_unique_fragment",
                "old": "СтарыйФрагмент",
                "control_fragment": "СтарыйФрагмент",
                "new": "НовыйФрагмент",
            },
            "resolve_origin": False,
        }
    )

    assert seen["ref"] == "Report.фс_ПродовольственнаяКорзина"
    assert seen["extension"] == "фс_Отчеты1"
    assert result["allowed"] is True
    assert result["route"]["name_resolution"]["status"] == "resolved"
    assert result["route"]["apply_payload_hint"]["payload"]["module_ref"] == "ConfigCASSave:extension-guid__form-guid.0"


def test_metadata_write_plan_resolves_public_form_selector_internally(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_resolve(payload: dict[str, Any]) -> dict[str, Any]:
        seen.update(payload)
        return {
            "schema": "onec_form_write_target_resolution.v1",
            "status": "ok",
            "source": {
                "kind": "live_sql",
                "table": "ConfigCASSave",
                "file_name": "extension-guid__form-guid.0",
            },
            "counts": {"matches": 1},
        }

    monkeypatch.setattr(adapter_server, "metadata_form_write_target_resolve", fake_resolve)

    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "form",
                "extension": "test2",
                "ref": "Catalog.test2",
                "form": "t_Форма",
                "command": "ЗаменаДомена",
            },
            "edits": [{"property": "Заголовок", "value": "Новое имя"}],
            "resolve_origin": False,
        }
    )

    assert seen["table"] == "ConfigCASSave"
    assert seen["ref"] == "Catalog.test2"
    assert seen["form"] == "t_Форма"
    assert seen["command"] == "ЗаменаДомена"
    assert seen["property"] == "Заголовок"
    assert seen["value"] == "Новое имя"
    assert seen["include_storage"] is True
    assert result["allowed"] is True
    assert result["status"] == "planned"
    assert result["route"]["name_resolution"] == {
        "status": "resolved",
        "method": "metadata.form.write_target.resolve",
        "selector": {
            "extension": "test2",
            "ref": "Catalog.test2",
            "form": "t_Форма",
            "command": "ЗаменаДомена",
        },
        "counts": {"matches": 1},
    }
    hint = result["route"]["apply_payload_hint"]
    assert hint["ready_for_apply_method"] is True
    assert "table" not in hint["payload"]
    assert "file_name" not in hint["payload"]
    assert hint["payload"]["extension"] == "test2"
    assert hint["payload"]["ref"] == "Catalog.test2"
    assert hint["payload"]["form"] == "t_Форма"
    assert hint["payload"]["command"] == "ЗаменаДомена"
    assert hint["payload"]["edits"] == [{"property": "Заголовок", "value": "Новое имя"}]


def test_metadata_write_plan_reports_unresolved_public_form_selector(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_form_write_target_resolve",
        lambda payload: {
            "schema": "onec_form_write_target_resolution.v1",
            "status": "not_found",
            "counts": {"matches": 0},
        },
    )

    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "form",
                "ref": "Catalog.Контрагенты",
                "form": "ФормаЭлемента",
                "element": "КнопкаЗаписать",
            },
            "edits": [{"property": "Заголовок", "value": "Записать"}],
            "resolve_origin": False,
        }
    )

    assert result["allowed"] is False
    assert result["status"] == "needs_origin"
    assert result["route"]["name_resolution"]["status"] == "not_found"
    assert any(problem["code"] == "public_form_target_not_resolved" for problem in result["problems"])


def test_write_gate_context_resolves_public_extension_name_to_guid(monkeypatch: pytest.MonkeyPatch) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    monkeypatch.setattr(
        adapter_server,
        "extension_filter_to_guid",
        lambda base_id, extension, method: (
            extension_guid,
            None,
        )
        if (base_id, extension, method) == ("upo_test", "test2", "write.gate_context")
        else (None, {"status": "not_found"}),
    )

    result = adapter_server.resolve_write_gate_context(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "form",
                "extension": "test2",
                "ref": "Catalog.test2",
                "form": "t_Форма",
                "command": "ЗаменаДомена",
            },
        }
    )

    assert result["extension_guid"] == extension_guid
    assert result["target"]["extension"] == "test2"
    assert result["target"]["extension_guid"] == extension_guid
    assert result["target"]["origin"]["extension"] == {
        "guid": extension_guid,
        "name": "test2",
    }
    assert result["target"]["origin"]["source"] == "extension"
    assert result["target"]["origin"]["status"] == "ok"
    assert result["development_layer_resolution"]["source"] == "extension_name"
    assert adapter_server.repository_control.development_layer_id(result) == f"extension:{extension_guid}"


def test_write_gate_context_does_not_fall_back_to_base_for_unknown_extension(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setattr(
        adapter_server,
        "extension_filter_to_guid",
        lambda *_args, **_kwargs: (
            None,
            {"status": "not_found", "error": "extension_not_found"},
        ),
    )

    result = adapter_server.resolve_write_gate_context(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "form",
                "extension": "missing-extension",
                "ref": "Catalog.test2",
                "form": "t_Форма",
            },
        }
    )

    assert "extension_guid" not in result
    assert result["development_layer_resolution"]["status"] == "not_resolved"
    assert result["development_layer_resolution"]["diagnostics"]["error"] == "extension_not_found"
    assert adapter_server.repository_control.development_layer_id(result) == "extension:unresolved"


def test_write_gate_context_resolves_concrete_saved_form_module_without_global_owner_scan(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    form_guid = "77494708-43ea-4956-ac3c-199cfb035ad2"
    file_name = f"{extension_guid}__{form_guid}.0"
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_modules_search",
        lambda payload: {
            "status": "ok",
            "modules": [
                {
                    "table": "ConfigCASSave",
                    "file_name": file_name,
                    "source": {"table": "ConfigCASSave", "file_name": file_name},
                    "owner": {"kind": "Catalog", "name": "test2", "guid": "owner-guid"},
                    "form": {"name": "t_Форма", "guid": form_guid},
                }
            ],
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "repository_resolve_form_guid_sql",
        lambda **_kwargs: pytest.fail("concrete saved form module must not start a global owner scan"),
    )

    result = adapter_server.resolve_write_gate_context(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "module",
                "module_ref": f"ConfigCASSave:{file_name}",
            },
            "timeout_seconds": 30,
        }
    )

    assert result["repository_object"] == "Catalog.test2.Form.t_Форма"
    assert result["extension_guid"] == extension_guid
    assert result["owner_resolution"]["source"] == "live_sql_saved_form_module"
    assert result["development_layer_resolution"]["layer_id"] == f"extension:{extension_guid}"


def test_write_gate_context_rejects_conflicting_extension_name_and_guid(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    name_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    supplied_guid = "11111111-1111-1111-1111-111111111111"
    monkeypatch.setattr(
        adapter_server,
        "extension_filter_to_guid",
        lambda base_id, extension, method: (name_guid, None),
    )

    result = adapter_server.resolve_write_gate_context(
        {
            "base_id": "upo_test",
            "extension_guid": supplied_guid,
            "target": {
                "kind": "form",
                "extension": "test2",
                "ref": "Catalog.test2",
                "form": "t_Форма",
            },
        }
    )

    resolution = result["development_layer_resolution"]
    assert "extension_guid" not in result
    assert "extension_guid" not in result["target"]
    assert resolution["status"] == "conflict"
    assert resolution["layer_id"] == "extension:unresolved"
    assert resolution["extension"] == {
        "name": "test2",
        "supplied_guid": supplied_guid,
    }
    assert resolution["diagnostics"]["error"] == "extension_selector_conflict"
    assert resolution["diagnostics"]["resolved_extension_guid"] == name_guid


def test_repository_apply_gate_blocks_unresolved_extension_before_policy_gates(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    def must_not_run(_payload: dict[str, Any]) -> dict[str, Any]:
        raise AssertionError("repository/support policy gates must not authorize an unresolved extension layer")

    monkeypatch.setattr(adapter_server.repository_control, "write_gate", must_not_run)
    monkeypatch.setattr(adapter_server, "live_sql_support_gate", must_not_run)

    result = adapter_server.repository_apply_gate(
        {
            "base_id": "upo_test",
            "development_layer_resolution": {
                "status": "conflict",
                "layer_id": "extension:unresolved",
                "diagnostics": {"error": "extension_selector_conflict"},
            },
        },
        "metadata.write",
        "apply",
    )

    assert result["status"] == "blocked"
    assert result["error"] == "extension_selector_conflict"
    assert result["layer_id"] == "extension:unresolved"


def test_metadata_write_apply_blocks_conflicting_extension_name_and_guid(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setattr(
        adapter_server,
        "extension_filter_to_guid",
        lambda base_id, extension, method: (
            "fb26cf42-7609-11f1-828f-005056b0d483",
            None,
        ),
    )

    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "extension_guid": "11111111-1111-1111-1111-111111111111",
            "target": {
                "kind": "form",
                "extension": "test2",
                "ref": "Catalog.test2",
                "form": "t_Форма",
                "command": "ЗаменаДомена",
            },
            "edits": [{"property": "Заголовок", "value": "Новое имя"}],
            "mode": "apply",
        }
    )

    assert result["schema"] == "onec_repository_write_gate.v1"
    assert result["status"] == "blocked"
    assert result["error"] == "extension_selector_conflict"
    assert result["development_layer_resolution"]["status"] == "conflict"


def test_metadata_write_preflight_routes_public_extension_form_gates_to_extension_layer(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    captured_layers: list[str] = []
    monkeypatch.setattr(
        adapter_server,
        "extension_filter_to_guid",
        lambda base_id, extension, method: (extension_guid, None),
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_form_write_target_resolve",
        lambda payload: {
            "schema": "onec_form_write_target_resolution.v1",
            "status": "ok",
            "source": {
                "kind": "live_sql",
                "table": "ConfigCASSave",
                "file_name": f"{extension_guid}__form-guid.0",
            },
            "counts": {"matches": 1},
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_diff",
        lambda payload: {
            "status": "changed",
            "target": {
                "table": payload["table"],
                "file_name": payload["file_name"],
            },
            "freshness": {
                "source": "live_sql",
                "status": "live_sql_verified",
                "verified_against_sql": True,
            },
        },
    )

    def fake_gate(payload: dict[str, Any]) -> dict[str, Any]:
        layer_id = adapter_server.repository_control.development_layer_id(payload)
        captured_layers.append(layer_id)
        return {"status": "ok", "allowed": True, "layer_id": layer_id}

    monkeypatch.setattr(adapter_server.repository_control, "write_gate", fake_gate)
    monkeypatch.setattr(adapter_server, "live_sql_support_gate", fake_gate)

    result = adapter_server.metadata_write_preflight(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "form",
                "extension": "test2",
                "ref": "Catalog.test2",
                "form": "t_Форма",
                "command": "ЗаменаДомена",
            },
            "edits": [{"property": "Заголовок", "value": "Новое имя"}],
            "resolve_origin": False,
        }
    )

    expected_layer = f"extension:{extension_guid}"
    assert result["status"] == "ready"
    assert result["allowed"] is True
    assert result["repository"]["layer_id"] == expected_layer
    assert result["support"]["layer_id"] == expected_layer
    assert captured_layers == [expected_layer, expected_layer]


def test_metadata_write_preflight_explains_missing_exact_extension_policy(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    extension_guid = "8e02accd-8a34-11f1-8294-005056b0d483"
    monkeypatch.setattr(
        adapter_server,
        "extension_filter_to_guid",
        lambda base_id, extension, method: (extension_guid, None),
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_form_write_target_resolve",
        lambda payload: {
            "schema": "onec_form_write_target_resolution.v1",
            "status": "ok",
            "source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": f"{extension_guid}__form-guid.0"},
            "counts": {"matches": 1},
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_diff",
        lambda payload: {"status": "changed", "target": {"table": payload["table"], "file_name": payload["file_name"]}},
    )
    layer_id = f"extension:{extension_guid}"
    monkeypatch.setattr(
        adapter_server.repository_control,
        "write_gate",
        lambda payload: {
            "status": "blocked_repository_configuration",
            "allowed": False,
            "layer_id": layer_id,
            "problem": {"status": "layer_not_configured"},
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "live_sql_support_gate",
        lambda payload: {"status": "blocked_support_layer_unknown", "allowed": False, "layer_id": layer_id},
    )

    result = adapter_server.metadata_write_preflight(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "form", "extension": "фс_Отчеты1", "ref": "Report.фс_ПродовольственнаяКорзина",
                "form": "ФормаОтчетаПродкорзина", "element": "КомпоновщикНастроекПользовательскиеНастройки",
            },
            "edits": [{"property": "Группировка", "value": "Horizontal"}],
            "resolve_origin": False,
        }
    )

    assert result["status"] == "blocked_repository_configuration"
    assert result["development_layer_configuration"] == {
        "status": "required",
        "error": "development_layer_not_configured",
        "layer_id": layer_id,
        "extension": "фс_Отчеты1",
        "message": "This exact extension has no repository/support development-layer policy in the adapter configuration. The adapter will not substitute another extension policy.",
        "next_action": "configure_exact_extension_development_layer",
    }
    assert result["development_layer_configuration_plan"]["status"] == "needs_configuration"
    assert result["development_layer_configuration_plan"]["target"] == {
        "kind": "extension", "extension": "фс_Отчеты1"
    }
    assert result["development_layer_configuration_plan"]["apply"]["requires_explicit_administrator_confirmation"] is True
    assert result["support"] == {
        "required": True,
        "allowed": False,
        "status": "deferred_until_repository_lock",
        "layer_id": layer_id,
        "reason": "repository_lock_required",
    }


def test_metadata_write_preflight_blocks_conflicting_extension_name_and_guid(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    name_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
    supplied_guid = "11111111-1111-1111-1111-111111111111"
    monkeypatch.setattr(
        adapter_server,
        "extension_filter_to_guid",
        lambda base_id, extension, method: (name_guid, None),
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_form_write_target_resolve",
        lambda payload: {
            "status": "ok",
            "source": {
                "table": "ConfigCASSave",
                "file_name": f"{name_guid}__form-guid.0",
            },
            "counts": {"matches": 1},
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_diff",
        lambda payload: {
            "status": "changed",
            "target": {"table": payload["table"], "file_name": payload["file_name"]},
            "freshness": {
                "source": "live_sql",
                "status": "live_sql_verified",
                "verified_against_sql": True,
            },
        },
    )
    monkeypatch.setattr(
        adapter_server.repository_control,
        "write_gate",
        lambda payload: {
            "status": "not_configured",
            "allowed": True,
            "layer_id": adapter_server.repository_control.development_layer_id(payload),
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "live_sql_support_gate",
        lambda payload: {
            "status": "support_not_configured_legacy",
            "allowed": True,
            "layer_id": adapter_server.repository_control.development_layer_id(payload),
        },
    )

    result = adapter_server.metadata_write_preflight(
        {
            "base_id": "upo_test",
            "extension_guid": supplied_guid,
            "target": {
                "kind": "form",
                "extension": "test2",
                "ref": "Catalog.test2",
                "form": "t_Форма",
                "command": "ЗаменаДомена",
            },
            "edits": [{"property": "Заголовок", "value": "Новое имя"}],
            "resolve_origin": False,
        }
    )

    assert result["status"] == "blocked"
    assert result["allowed"] is False
    assert result["repository"]["allowed"] is True
    assert result["support"]["allowed"] is True
    assert result["development_layer_resolution"]["status"] == "conflict"
    assert result["development_layer_resolution"]["diagnostics"]["error"] == "extension_selector_conflict"


def test_metadata_write_plan_help_describes_name_first_form_and_module_targets() -> None:
    methods = {
        row["name"]: row
        for row in adapter_server.call_method("help.methods", {})["methods"]
    }

    for method_name in ("metadata.write.plan", "metadata.write.preflight"):
        method = methods[method_name]
        target_properties = method["input_schema"]["properties"]["target"]["properties"]
        assert method["input_schema"]["required"] == ["base_id", "target"]
        assert {"ref", "extension", "form", "element", "command", "attribute", "module", "qualified_name", "stream_ordinal"} <= set(target_properties)
        assert method["input_schema"]["properties"]["edits"]["items"]["required"] == ["property", "value"]
        examples = method["input_schema"]["examples"]
        assert any((example.get("target") or {}).get("kind") == "form" and (example.get("target") or {}).get("ref") for example in examples)
        assert any((example.get("target") or {}).get("kind") == "module" and (example.get("target") or {}).get("module") for example in examples)

    description = methods["metadata.write.plan"]["description"]
    assert "name-first" in description
    assert "metadata.saved_state.modules.search" in description


def test_metadata_write_preflight_verifies_concrete_saved_state(monkeypatch: pytest.MonkeyPatch) -> None:
    def fake_diff(payload: dict[str, Any]) -> dict[str, Any]:
        assert payload["module_ref"] == "ConfigCASSave:object-guid__module-guid.0#stream:4"
        return {
            "schema": "onec_saved_state_diff.v1",
            "method": "metadata.saved_state.diff",
            "status": "changed",
            "target": {
                "table": "ConfigCASSave",
                "file_name": "object-guid__module-guid.0",
                "module_ref": payload["module_ref"],
            },
            "source": {"kind": "live_sql"},
            "current_state": {"source": "saved_state", "activation_state": "not_activated"},
            "needs_prepare": False,
            "comparison": {"differs": True},
            "freshness": {
                "source": "live_sql",
                "status": "live_sql_verified",
                "verified_against_sql": True,
                "active_payload_sha1": "active",
                "saved_payload_sha1": "saved",
            },
        }

    monkeypatch.setattr(adapter_server, "metadata_saved_state_diff", fake_diff)

    result = adapter_server.metadata_write_preflight(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "module",
                "module_ref": "ConfigCASSave:object-guid__module-guid.0#stream:4",
            },
            "intent": {"operation": "replace", "old": "a", "new": "b"},
        }
    )

    assert result["schema"] == "onec_metadata_write_preflight.v1"
    assert result["status"] == "ready"
    assert result["allowed"] is True
    assert result["route"]["writer"] == "metadata.module.write_apply"
    assert result["saved_state"]["status"] == "changed"
    assert result["saved_state"]["freshness"]["status"] == "live_sql_verified"
    assert result["guards"]["rollback_available"] is True
    assert result["write_context"]["module_ref"] == "ConfigCASSave:object-guid__module-guid.0#stream:4"
    assert result["write_context"]["table"] == "ConfigCASSave"
    assert result["write_context"]["file_name"] == "object-guid__module-guid.0"
    assert result["write_context"]["stream_index"] == 4


def test_metadata_write_preflight_reports_prepare_needed(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_diff",
        lambda payload: {
            "schema": "onec_saved_state_diff.v1",
            "method": "metadata.saved_state.diff",
            "status": "not_found",
            "error": "saved_state_not_found",
            "target": {"table": "ConfigCASSave", "file_name": "object-guid__module-guid.0"},
            "needs_prepare": True,
            "prepare_payload": {
                "method": "metadata.saved_state.prepare",
                "base_id": payload["base_id"],
                "target_table": payload["table"],
                "file_name": payload["file_name"],
                "mode": "plan",
            },
        },
    )

    result = adapter_server.metadata_write_preflight(
        {
            "base_id": "upo_test",
            "target": {"kind": "module", "file_name": "object-guid__module-guid.0", "stream_index": 0, "table": "ConfigCASSave"},
            "intent": {"operation": "replace", "old": "a", "new": "b"},
        }
    )

    assert result["status"] == "needs_prepare"
    assert result["allowed"] is False
    assert result["saved_state"]["needs_prepare"] is True
    assert result["saved_state"]["prepare_payload"]["method"] == "metadata.saved_state.prepare"


def test_metadata_write_preflight_blocks_effective_path_without_route() -> None:
    result = adapter_server.metadata_write_preflight(
        {
            "base_id": "upo_test",
            "target": {"kind": "form", "canonical_path": "Справочник.Контрагенты.Наименование"},
            "intent": {"operation": "property_change", "property": "Заголовок", "value": "Контрагент"},
        }
    )

    assert result["status"] in {"blocked", "needs_resolution"}
    assert result["allowed"] is False
    assert result["plan"]["allowed"] is False
    assert result["diagnostics"]["read_only"] is True


def test_metadata_write_plan_routes_container_module_ref_to_code_write() -> None:
    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "module",
                "module_ref": "ConfigCASSave:extension-guid__form-guid.0",
                "routine_name": "ЗаменаДомена",
            },
            "intent": {"operation": "replace", "old": "a", "new": "b"},
        }
    )

    hint = result["route"]["apply_payload_hint"]
    assert result["allowed"] is True
    assert hint["method"] == "code.write"
    assert hint["ready_for_apply_method"] is True
    assert hint["routing"]["reason"] == "container_module_requires_agent_code_write"
    assert "#stream:" not in hint["payload"]["module_ref"]


def test_metadata_write_plan_preserves_module_file_name_reference() -> None:
    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "module",
                "file_name": "object-guid__module-guid.0",
                "stream_index": 4,
            },
            "intent": {"operation": "replace", "old": "a", "new": "b"},
        }
    )

    hint_payload = result["route"]["apply_payload_hint"]["payload"]
    assert result["allowed"] is True
    assert result["target"]["concrete_reference_field"] == "file_name"
    assert hint_payload["file_name"] == "object-guid__module-guid.0"
    assert hint_payload["stream_index"] == 4
    assert "module_ref" not in hint_payload


def test_metadata_write_plan_preserves_form_guid_reference() -> None:
    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "form",
                "form_guid": "form-guid",
            },
            "intent": {"operation": "property_change", "property": "Заголовок", "value": "Новый"},
        }
    )

    hint_payload = result["route"]["apply_payload_hint"]["payload"]
    assert result["allowed"] is True
    assert result["target"]["concrete_reference_field"] == "form_guid"
    assert hint_payload["form_guid"] == "form-guid"
    assert "file_name" not in hint_payload


def test_metadata_write_plan_blocks_form_guid_for_module_target() -> None:
    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "module",
                "form_guid": "form-guid",
            },
            "intent": {"operation": "replace", "old": "a", "new": "b"},
        }
    )

    assert result["allowed"] is False
    assert result["status"] == "blocked"
    mismatch = [problem for problem in result["problems"] if problem["code"] == "concrete_reference_kind_mismatch"][0]
    assert mismatch["concrete_reference_field"] == "form_guid"
    assert mismatch["target_kind"] == "module"


def test_metadata_write_plan_blocks_module_ref_for_form_target() -> None:
    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "form",
                "module_ref": "ConfigCASSave:object__module.0#stream:4",
            },
            "intent": {"operation": "property_change", "property": "Заголовок", "value": "Новый"},
        }
    )

    assert result["allowed"] is False
    assert result["status"] == "blocked"
    mismatch = [problem for problem in result["problems"] if problem["code"] == "concrete_reference_kind_mismatch"][0]
    assert mismatch["concrete_reference_field"] == "module_ref"
    assert mismatch["target_kind"] == "form"


def test_metadata_write_plan_blocks_replace_with_control_without_control_fragment() -> None:
    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "module",
                "module_ref": "ConfigCASSave:object-guid__module-guid.0#stream:4",
            },
            "intent": {"operation": "replace_with_control", "new": "Сообщить(\"new\");"},
        }
    )

    assert result["allowed"] is False
    assert result["status"] == "blocked"
    assert "controlled_fragment_matches_current_source" in result["required_guards"]
    assert any(problem["code"] == "missing_control_fragment" for problem in result["problems"])


def test_metadata_write_plan_normalizes_russian_replace_with_control() -> None:
    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "module",
                "module_ref": "ConfigCASSave:object-guid__module-guid.0#stream:4",
            },
            "intent": {"operation": "вместо с контролем", "new": "Сообщить(\"new\");"},
        }
    )

    assert result["route"]["operation"] == "вместо с контролем"
    assert result["route"]["operation_class"] == "replace_with_control"
    assert "controlled_fragment_matches_current_source" in result["required_guards"]
    assert any(problem["code"] == "missing_control_fragment" for problem in result["problems"])


def test_metadata_write_plan_allows_replace_with_control_with_control_fragment() -> None:
    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "module",
                "module_ref": "ConfigCASSave:object-guid__module-guid.0#stream:4",
            },
            "intent": {
                "operation": "replace_with_control",
                "control_fragment": "Сообщить(\"old\");",
                "new": "Сообщить(\"new\");",
            },
        }
    )

    assert result["allowed"] is True
    assert result["status"] == "planned"
    assert result["problems"] == []
    hint_payload = result["route"]["apply_payload_hint"]["payload"]
    assert hint_payload["module_ref"] == "ConfigCASSave:object-guid__module-guid.0#stream:4"
    assert hint_payload["expected_old_contains"] == "Сообщить(\"old\");"
    assert hint_payload["new"] == "Сообщить(\"new\");"


def test_metadata_write_plan_normalizes_russian_insert_after_with_anchor() -> None:
    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "module",
                "module_ref": "ConfigCASSave:object-guid__module-guid.0#stream:4",
            },
            "intent": {
                "operation": "вставить после",
                "anchor": "Сообщить(\"anchor\");",
                "new": "Сообщить(\"after\");",
            },
        }
    )

    assert result["allowed"] is True
    assert result["status"] == "planned"
    assert result["route"]["operation_class"] == "insert_after"
    assert result["problems"] == []
    hint_payload = result["route"]["apply_payload_hint"]["payload"]
    assert hint_payload["operation"] == "insert_after"
    assert hint_payload["expected_contains"] == "Сообщить(\"anchor\");"


def test_metadata_write_plan_blocks_insert_without_anchor() -> None:
    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "module",
                "module_ref": "ConfigCASSave:object-guid__module-guid.0#stream:4",
            },
            "intent": {"operation": "insert_after", "new": "Сообщить(\"after\");"},
        }
    )

    assert result["allowed"] is False
    assert result["status"] == "blocked"
    assert any(problem["code"] == "missing_insert_anchor" for problem in result["problems"])


def test_metadata_write_plan_blocks_replace_without_new_code() -> None:
    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "module",
                "module_ref": "ConfigCASSave:object-guid__module-guid.0#stream:4",
            },
            "intent": {"operation": "replace", "old": "Сообщить(\"old\");"},
        }
    )

    assert result["allowed"] is False
    assert result["status"] == "blocked"
    assert any(problem["code"] == "missing_new_code" for problem in result["problems"])


def test_metadata_write_plan_infers_operation_from_extension_action() -> None:
    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "module",
                "module_ref": "ConfigCASSave:object-guid__module-guid.0#stream:4",
                "extension_action": {"status": "ok", "operation_class": "replace_with_control"},
            },
            "intent": {
                "control_fragment": "Сообщить(\"old\");",
                "new": "Сообщить(\"new\");",
            },
        }
    )

    assert result["allowed"] is True
    assert result["route"]["operation_class"] == "replace_with_control"
    assert result["route"]["operation_inferred_from"] == "extension_action"
    assert result["route"]["apply_payload_hint"]["payload"]["expected_old_contains"] == "Сообщить(\"old\");"


def test_metadata_write_plan_blocks_unknown_extension_action() -> None:
    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "module",
                "module_ref": "ConfigCASSave:object-guid__module-guid.0#stream:4",
                "extension_action": {"status": "unknown", "operation_class": "unknown_extension_action"},
            },
            "intent": {"new": "Сообщить(\"new\");"},
        }
    )

    assert result["allowed"] is False
    assert result["status"] == "blocked"
    assert any(problem["code"] == "extension_action_unknown" for problem in result["problems"])


def test_metadata_write_plan_blocks_extension_action_operation_mismatch() -> None:
    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "module",
                "module_ref": "ConfigCASSave:object-guid__module-guid.0#stream:4",
                "extension_action": {"status": "ok", "operation_class": "insert_before"},
            },
            "intent": {"operation": "replace", "old": "a", "new": "b"},
        }
    )

    assert result["allowed"] is False
    assert result["status"] == "blocked"
    mismatch = [problem for problem in result["problems"] if problem["code"] == "extension_action_operation_mismatch"][0]
    assert mismatch["requested_operation"] == "replace"
    assert mismatch["extension_operation"] == "insert_before"


def test_metadata_write_plan_blocks_ambiguous_extension_actions() -> None:
    result = adapter_server.metadata_write_plan(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "module",
                "module_ref": "ConfigCASSave:object-guid__module-guid.0#stream:4",
            },
            "extension_actions": [
                {"status": "ok", "operation_class": "insert_before"},
                {"status": "ok", "operation_class": "replace"},
            ],
            "intent": {"operation": "replace", "old": "a", "new": "b"},
        }
    )

    assert result["allowed"] is False
    assert result["status"] == "blocked"
    ambiguous = [problem for problem in result["problems"] if problem["code"] == "extension_action_ambiguous"][0]
    assert len(ambiguous["extension_actions"]) == 2
    assert len(result["route"]["extension_actions"]) == 2


def test_metadata_write_blocks_canonical_path_without_write_plan_route() -> None:
    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "target": {"kind": "form", "canonical_path": "Справочник.Контрагенты.Наименование"},
            "mode": "plan",
            "edits": [{"property": "Заголовок", "value": "Контрагент"}],
        }
    )

    assert result["schema"] == "onec_metadata_write.v1"
    assert result["status"] == "blocked"
    assert result["error"] == "write_plan_required"
    assert result["routed_method"] == "metadata.write.plan"
    assert result["plan"]["allowed"] is False
    assert result["plan"]["target"]["canonical_path"] == "Справочник.Контрагенты.Наименование"


def test_metadata_write_blocked_form_path_exposes_next_resolution() -> None:
    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "target": {"canonical_path": "Документ.Заказ.Форма.ФормаДокумента.КнопкаЗаписать"},
            "mode": "plan",
            "edits": [{"property": "Заголовок", "value": "Записать"}],
        }
    )

    assert result["status"] == "blocked"
    assert result["target_kind"] == "form"
    assert result["next_resolution"]["method"] == adapter_server.FORM_WRITE_TARGET_RESOLVE_METHOD
    assert result["next_resolution"]["params"]["ref"] == "Document.Заказ"
    assert result["next_resolution"]["params"]["form"] == "ФормаДокумента"
    assert result["next_resolution"]["params"]["element"] == "КнопкаЗаписать"
    assert "payload" not in result["next_resolution"]
    assert result["apply_payload_hint"]["ready_for_apply_method"] is False
    assert result["apply_payload_hint"]["payload"]["form"] == "ФормаДокумента"
    assert result["apply_payload_hint"]["payload"]["element"] == "КнопкаЗаписать"


def test_metadata_write_blocked_module_path_exposes_next_resolution() -> None:
    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "target": {"canonical_path": "ОбщийМодуль.Интеграция.Отправить"},
            "intent": {"operation": "replace", "old": "Сообщить(\"old\");", "new": "Сообщить(\"new\");"},
        }
    )

    assert result["status"] == "blocked"
    assert result["target_kind"] == "module"
    assert result["next_resolution"]["method"] == adapter_server.SAVED_STATE_MODULES_SEARCH_METHOD
    assert result["next_resolution"]["params"]["ref"] == "CommonModule.Интеграция"
    assert result["next_resolution"]["params"]["query"] == "Отправить"
    assert "payload" not in result["next_resolution"]
    assert result["apply_payload_hint"]["ready_for_apply_method"] is False
    assert result["apply_payload_hint"]["payload"]["kind"] == "CommonModule"
    assert result["apply_payload_hint"]["payload"]["name"] == "Интеграция"
    assert result["apply_payload_hint"]["payload"]["routine_name"] == "Отправить"


def test_metadata_write_uses_form_path_hint_with_concrete_file(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
        seen["payload"] = payload
        return {"schema": "onec_form_element_write_apply.v1", "status": "planned"}

    monkeypatch.setattr(adapter_server, "metadata_form_element_write_apply", fake_apply)

    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "target": {
                "canonical_path": "Документ.Заказ.Форма.ФормаДокумента.КнопкаЗаписать",
                "file_name": "form-guid.0",
            },
            "mode": "plan",
            "edits": [{"property": "Заголовок", "value": "Записать"}],
        }
    )

    assert result["status"] == "planned"
    assert result["target_kind"] == "form"
    assert seen["payload"]["file_name"] == "form-guid.0"
    assert seen["payload"]["form"] == "ФормаДокумента"
    assert seen["payload"]["element"] == "КнопкаЗаписать"


def test_metadata_write_uses_form_path_hint_with_form_guid(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
        seen["payload"] = payload
        return {"schema": "onec_form_element_write_apply.v1", "status": "planned"}

    monkeypatch.setattr(adapter_server, "metadata_form_element_write_apply", fake_apply)

    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "target": {
                "canonical_path": "Документ.Заказ.Форма.ФормаДокумента.КнопкаЗаписать",
                "form_guid": "form-guid",
            },
            "mode": "plan",
            "edits": [{"property": "Заголовок", "value": "Записать"}],
        }
    )

    assert result["status"] == "planned"
    assert seen["payload"]["form_guid"] == "form-guid"
    assert "file_name" not in seen["payload"]
    assert seen["payload"]["form"] == "ФормаДокумента"
    assert seen["payload"]["element"] == "КнопкаЗаписать"


def test_metadata_write_uses_module_path_hint_with_concrete_ref(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
        seen["payload"] = payload
        return {"schema": "onec_module_write_apply.v1", "status": "planned"}

    monkeypatch.setattr(adapter_server, "metadata_module_write_apply", fake_apply)

    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "target": {
                "canonical_path": "ОбщийМодуль.Интеграция.Отправить",
                "module_ref": "ConfigCASSave:common_module.0#stream:0",
            },
            "old": "Сообщить(\"old\");",
            "new": "Сообщить(\"new\");",
        }
    )

    assert result["status"] == "planned"
    assert result["target_kind"] == "module"
    assert seen["payload"]["module_ref"] == "ConfigCASSave:common_module.0#stream:0"
    assert seen["payload"]["routine_name"] == "Отправить"
    assert seen["payload"]["old"] == "Сообщить(\"old\");"
    assert seen["payload"]["new"] == "Сообщить(\"new\");"


def test_metadata_write_routes_container_module_ref_to_embedded_writer(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
        raise AssertionError("metadata.write must not call stream writer for container module refs")

    def fake_embedded(payload: dict[str, Any], **kwargs: Any) -> dict[str, Any]:
        seen["payload"] = payload
        seen["kwargs"] = kwargs
        return {"schema": "onec_form_embedded_module_write.v1", "status": "planned"}

    monkeypatch.setattr(adapter_server, "metadata_module_write_apply", fake_apply)
    monkeypatch.setattr(adapter_server, "form_embedded_module_handler_write_apply", fake_embedded)

    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "module",
                "module_ref": "ConfigCASSave:extension-guid__form-guid.0",
                "routine_name": "ЗаменаДомена",
            },
            "old": "a",
            "new": "b",
        }
    )

    assert result["status"] == "planned"
    assert result["routed_method"] == "form_embedded_module_handler_write_apply"
    assert seen["kwargs"]["table"] == "ConfigCASSave"
    assert seen["kwargs"]["file_name"] == "extension-guid__form-guid.0"
    assert seen["kwargs"]["handler_name"] == "ЗаменаДомена"
    assert seen["payload"]["routine_text"] == "b"


def test_metadata_write_routes_resolved_form_embedded_module_to_embedded_writer(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_search(payload: dict[str, Any]) -> dict[str, Any]:
        seen["search_payload"] = payload
        return {
            "status": "ok",
            "modules": [
                {
                    "payload": {"sha1": "a" * 40, "role": "form_embedded_module_payload"},
                    "streams": [
                        {
                            "module_ref": "ConfigCASSave:extension-guid__form-guid.0",
                            "module_path": "2",
                        }
                    ],
                }
            ],
            "counts": {"modules": 1, "scanned": 1},
        }

    def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
        raise AssertionError("metadata.write must not call stream writer for resolved form container refs")

    def fake_embedded(payload: dict[str, Any], **kwargs: Any) -> dict[str, Any]:
        seen["payload"] = payload
        seen["kwargs"] = kwargs
        return {"schema": "onec_form_embedded_module_write.v1", "status": "planned"}

    monkeypatch.setattr(adapter_server, "metadata_saved_state_modules_search", fake_search)
    monkeypatch.setattr(adapter_server, "metadata_module_write_apply", fake_apply)
    monkeypatch.setattr(adapter_server, "form_embedded_module_handler_write_apply", fake_embedded)

    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "module",
                "object_type": "CommonForm",
                "object_name": "t_Форма",
            },
            "routine_name": "ЗаменаДомена",
            "routine_text": "Процедура ЗаменаДомена(Команда)\nКонецПроцедуры\n",
            "routine_operation": "replace",
        }
    )

    assert result["status"] == "planned"
    assert result["routed_method"] == "form_embedded_module_handler_write_apply"
    assert seen["search_payload"]["object_type"] == "CommonForm"
    assert seen["search_payload"]["object_name"] == "t_Форма"
    assert seen["kwargs"]["table"] == "ConfigCASSave"
    assert seen["kwargs"]["file_name"] == "extension-guid__form-guid.0"
    assert seen["kwargs"]["handler_name"] == "ЗаменаДомена"
    assert seen["payload"]["module_path"] == "2"
    assert seen["payload"]["expected_sha1"] == "a" * 40


def test_metadata_write_routes_common_form_routine_path_to_embedded_writer(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_search(payload: dict[str, Any]) -> dict[str, Any]:
        seen["search_payload"] = payload
        return {
            "status": "ok",
            "modules": [
                {
                    "payload": {"sha1": "b" * 40, "role": "form_embedded_module_payload"},
                    "streams": [
                        {
                            "module_ref": "ConfigCASSave:extension-guid__form-guid.0",
                            "module_path": "2",
                        }
                    ],
                }
            ],
            "counts": {"modules": 1, "scanned": 1},
        }

    def fake_embedded(payload: dict[str, Any], **kwargs: Any) -> dict[str, Any]:
        seen["payload"] = payload
        seen["kwargs"] = kwargs
        return {"schema": "onec_form_embedded_module_write.v1", "status": "planned"}

    monkeypatch.setattr(adapter_server, "metadata_saved_state_modules_search", fake_search)
    monkeypatch.setattr(adapter_server, "form_embedded_module_handler_write_apply", fake_embedded)

    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "target": {"canonical_path": "ОбщаяФорма.t_Форма.ЗаменаДомена"},
            "routine_text": "Процедура ЗаменаДомена(Команда)\nКонецПроцедуры\n",
            "routine_operation": "replace",
        }
    )

    assert result["status"] == "planned"
    assert result["target_kind"] == "module"
    assert result["routed_method"] == "form_embedded_module_handler_write_apply"
    assert seen["search_payload"]["object_type"] == "CommonForm"
    assert seen["search_payload"]["object_name"] == "t_Форма"
    assert seen["search_payload"]["query"] == "ЗаменаДомена"
    assert seen["kwargs"]["handler_name"] == "ЗаменаДомена"
    assert seen["payload"]["module_path"] == "2"
    assert seen["payload"]["expected_sha1"] == "b" * 40


def test_metadata_write_common_form_upsert_searches_owner_without_new_routine_query(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_search(payload: dict[str, Any]) -> dict[str, Any]:
        seen["search_payload"] = payload
        return {
            "status": "ok",
            "modules": [
                {
                    "payload": {"sha1": "d" * 40, "role": "form_embedded_module_payload"},
                    "streams": [{"module_ref": "ConfigCASSave:extension-guid__form-guid.0", "module_path": "2"}],
                }
            ],
            "counts": {"modules": 1, "scanned": 1},
        }

    def fake_embedded(payload: dict[str, Any], **kwargs: Any) -> dict[str, Any]:
        seen["payload"] = payload
        seen["kwargs"] = kwargs
        return {"schema": "onec_form_embedded_module_write.v1", "status": "planned"}

    monkeypatch.setattr(adapter_server, "metadata_saved_state_modules_search", fake_search)
    monkeypatch.setattr(adapter_server, "form_embedded_module_handler_write_apply", fake_embedded)

    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "target": {"canonical_path": "ОбщаяФорма.t_Форма.РасчетС"},
            "routine_text": "Процедура РасчетС(Команда)\nКонецПроцедуры\n",
            "routine_operation": "upsert",
        }
    )

    assert result["status"] == "planned"
    assert result["routed_method"] == "form_embedded_module_handler_write_apply"
    assert seen["search_payload"]["object_type"] == "CommonForm"
    assert seen["search_payload"]["object_name"] == "t_Форма"
    assert "query" not in seen["search_payload"]
    assert seen["kwargs"]["handler_name"] == "РасчетС"


def test_metadata_write_apply_defaults_to_save_first_flags(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_search(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "status": "ok",
            "modules": [
                {
                    "payload": {"sha1": "c" * 40, "role": "form_embedded_module_payload"},
                    "streams": [{"module_ref": "ConfigCASSave:extension-guid__form-guid.0", "module_path": "2"}],
                }
            ],
            "counts": {"modules": 1, "scanned": 1},
        }

    def fake_embedded(payload: dict[str, Any], **kwargs: Any) -> dict[str, Any]:
        seen["payload"] = payload
        seen["kwargs"] = kwargs
        return {"schema": "onec_form_embedded_module_write.v1", "status": "applied", "applied": True}

    monkeypatch.setattr(adapter_server, "metadata_saved_state_modules_search", fake_search)
    monkeypatch.setattr(adapter_server, "form_embedded_module_handler_write_apply", fake_embedded)

    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "mode": "apply",
            "target": {"canonical_path": "ОбщаяФорма.t_Форма.ЗаменаДомена"},
            "routine_text": "Процедура ЗаменаДомена(Команда)\nКонецПроцедуры\n",
            "routine_operation": "replace",
        }
    )

    assert result["status"] == "applied"
    assert result["routed_method"] == "form_embedded_module_handler_write_apply"
    assert seen["kwargs"]["mode"] == "apply"
    assert seen["payload"]["allow_sql_saved_state_apply"] is True
    assert seen["payload"]["allow_sql_saved_state_prepare"] is True
    assert seen["payload"]["auto_prepare_saved_state"] is True


def test_preserve_bsl_routine_directives_keeps_existing_form_directive() -> None:
    current = "\r\n&НаКлиенте\r\nПроцедура ЗаменаДомена(Команда)\r\n\t// comment\r\nКонецПроцедуры\r\n"
    replacement = "Процедура ЗаменаДомена(Команда)\r\nКонецПроцедуры\r\n"

    result = adapter_server.preserve_bsl_routine_directives(current, replacement, "ЗаменаДомена")

    assert result == "&НаКлиенте\r\nПроцедура ЗаменаДомена(Команда)\r\nКонецПроцедуры\r\n"


def test_preserve_bsl_routine_directives_keeps_explicit_replacement_directive() -> None:
    current = "\r\n&НаКлиенте\r\nПроцедура ЗаменаДомена(Команда)\r\nКонецПроцедуры\r\n"
    replacement = "&НаСервере\r\nПроцедура ЗаменаДомена(Команда)\r\nКонецПроцедуры\r\n"

    result = adapter_server.preserve_bsl_routine_directives(current, replacement, "ЗаменаДомена")

    assert result == replacement


def test_form_embedded_module_apply_blocks_canonicalized_payload(monkeypatch: pytest.MonkeyPatch) -> None:
    original_text = '{"meta","data","\r\n&НаКлиенте\r\nПроцедура Cmd(Команда)\r\n\t// old\r\nКонецПроцедуры\r\n"}'
    original = compress_payload(original_text.encode("utf-8-sig"), "raw_deflate")
    canonicalized_text = '{"meta","data","\r\n&НаКлиенте\r\nПроцедура Cmd(Команда)\r\nКонецПроцедуры\r\n","extra"}'
    canonicalized = compress_payload(canonicalized_text.encode("utf-8-sig"), "raw_deflate")

    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda *args, **kwargs: (original, {"database": "upo_test"}, None),
    )
    monkeypatch.setattr(
        adapter_server,
        "changes_propose",
        lambda payload: {
            "schema": "onec_change_proposal.v1",
            "status": "accepted_for_review",
            "source": {"table": "ConfigCASSave", "file_name": "form.0"},
            "original": {"sha1": hashlib.sha1(original).hexdigest(), "bytes": len(original)},
            "encoded": {"sha1": hashlib.sha1(canonicalized).hexdigest(), "bytes": len(canonicalized), "payload_hex": canonicalized.hex()},
            "edits": [{"mode": "path_preserve_format", "path": "2"}],
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "storage_saved_state_apply_proposal",
        lambda payload: (_ for _ in ()).throw(AssertionError("unsafe payload must not be applied")),
    )

    result = adapter_server.form_embedded_module_handler_write_apply(
        {
            "routine_text": "Процедура Cmd(Команда)\nКонецПроцедуры\n",
            "allow_sql_saved_state_apply": True,
        },
        base_id="upo_test",
        table="ConfigCASSave",
        file_name="form.0",
        handler_name="Cmd",
        mode="apply",
        timeout_seconds=30,
        method_name="metadata.write",
    )

    assert result["status"] == "blocked"
    assert result["error"] == "unsafe_form_module_payload_write"


def test_storage_saved_state_apply_blocks_canonical_form_module_edit() -> None:
    proposal = {
        "schema": "onec_change_proposal.v1",
        "status": "accepted_for_review",
        "source": {"table": "ConfigCASSave", "file_name": "form.0"},
        "original": {"sha1": "a" * 40, "bytes": 1},
        "encoded": {"sha1": "b" * 40, "bytes": 1, "payload_hex": "00"},
        "edits": [{"mode": "path", "path": "2"}],
    }

    result = adapter_server.storage_saved_state_apply_proposal(
        {"base_id": "upo_test", "allow_sql_saved_state_apply": True, "proposal": proposal}
    )

    assert result["status"] == "blocked"
    assert result["error"] == "unsafe_form_module_payload_write"
    assert result["applied"] is False


def test_form_embedded_module_apply_allows_single_path_preserve_patch(monkeypatch: pytest.MonkeyPatch) -> None:
    original_text = '{"meta","data","\r\n&НаКлиенте\r\nПроцедура Cmd(Команда)\r\n\t// old\r\nКонецПроцедуры\r\n"}'
    original = compress_payload(original_text.encode("utf-8-sig"), "raw_deflate")
    seen: dict[str, Any] = {}

    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda *args, **kwargs: (original, {"database": "upo_test"}, None),
    )
    def fake_changes_propose(payload: dict[str, Any]) -> dict[str, Any]:
        expected_text, _info = patch_brace_text_path(
            decode_payload_lossless(original)["text"],
            "2",
            payload["edits"][0]["value"],
        )
        encoded = encode_payload_lossless(decode_payload_lossless(original), text=expected_text)
        return {
            "schema": "onec_change_proposal.v1",
            "status": "accepted_for_review",
            "source": {"table": "ConfigCASSave", "file_name": "form.0"},
            "original": {"sha1": hashlib.sha1(original).hexdigest(), "bytes": len(original)},
            "encoded": {"sha1": hashlib.sha1(encoded).hexdigest(), "bytes": len(encoded), "payload_hex": encoded.hex()},
            "edits": [{"mode": "path_preserve_format", "path": "2"}],
        }

    monkeypatch.setattr(adapter_server, "changes_propose", fake_changes_propose)

    def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
        seen["apply"] = payload
        return {"schema": "onec_storage_saved_state_apply.v1", "status": "applied", "applied": True, "readback": {"verified": True}}

    monkeypatch.setattr(adapter_server, "storage_saved_state_apply_proposal", fake_apply)

    result = adapter_server.form_embedded_module_handler_write_apply(
        {
            "routine_text": "Процедура Cmd(Команда)\nКонецПроцедуры\n",
            "allow_sql_saved_state_apply": True,
        },
        base_id="upo_test",
        table="ConfigCASSave",
        file_name="form.0",
        handler_name="Cmd",
        mode="apply",
        timeout_seconds=30,
        method_name="metadata.write",
    )

    assert result["status"] == "applied"
    assert seen["apply"]["allow_sql_saved_state_apply"] is True


def test_form_embedded_module_full_text_preserves_trailing_marker() -> None:
    current = "&НаКлиенте\nПроцедура Старая()\nКонецПроцедуры\n\n///----"
    new = "&НаКлиенте\nПроцедура Новая()\nКонецПроцедуры\n"

    result = adapter_server.preserve_form_embedded_module_suffix(current, new)

    assert result == "&НаКлиенте\nПроцедура Новая()\nКонецПроцедуры\n\n///----"


def test_form_embedded_module_apply_replaces_unique_fragment(monkeypatch: pytest.MonkeyPatch) -> None:
    original_text = '{"meta","data","\r\n&AtClient\r\nProcedure Cmd(Command)\r\n\tLine_old;\r\nEndProcedure\r\n"}'
    original = compress_payload(original_text.encode("utf-8-sig"), "raw_deflate")
    seen: dict[str, Any] = {}

    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda *args, **kwargs: (original, {"database": "upo_test"}, None),
    )

    def fake_changes_propose(payload: dict[str, Any]) -> dict[str, Any]:
        seen["new_text"] = payload["edits"][0]["value"]
        expected_text, _info = patch_brace_text_path(
            decode_payload_lossless(original)["text"],
            "2",
            payload["edits"][0]["value"],
        )
        encoded = encode_payload_lossless(decode_payload_lossless(original), text=expected_text)
        return {
            "schema": "onec_change_proposal.v1",
            "status": "accepted_for_review",
            "source": {"table": "ConfigCASSave", "file_name": "form.0"},
            "original": {"sha1": hashlib.sha1(original).hexdigest(), "bytes": len(original)},
            "encoded": {"sha1": hashlib.sha1(encoded).hexdigest(), "bytes": len(encoded), "payload_hex": encoded.hex()},
            "edits": [{"mode": "path_preserve_format", "path": "2"}],
        }

    monkeypatch.setattr(adapter_server, "changes_propose", fake_changes_propose)
    monkeypatch.setattr(
        adapter_server,
        "storage_saved_state_apply_proposal",
        lambda payload: {"schema": "onec_storage_saved_state_apply.v1", "status": "applied", "applied": True, "readback": {"verified": True}},
    )

    result = adapter_server.form_embedded_module_handler_write_apply(
        {
            "old": "Line_old;",
            "new": "Line_new;",
            "allow_sql_saved_state_apply": True,
        },
        base_id="upo_test",
        table="ConfigCASSave",
        file_name="form.0",
        handler_name="",
        mode="apply",
        timeout_seconds=30,
        method_name="metadata.write",
    )

    assert result["status"] == "applied"
    assert "Line_new;" in seen["new_text"]


def test_form_embedded_module_apply_blocks_ambiguous_fragment(monkeypatch: pytest.MonkeyPatch) -> None:
    original_text = '{"meta","data","\r\nProcedure Cmd(Command)\r\n\tLine_x;\r\n\tLine_x;\r\nEndProcedure\r\n"}'
    original = compress_payload(original_text.encode("utf-8-sig"), "raw_deflate")

    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda *args, **kwargs: (original, {"database": "upo_test"}, None),
    )
    monkeypatch.setattr(
        adapter_server,
        "changes_propose",
        lambda payload: (_ for _ in ()).throw(AssertionError("ambiguous fragment must not be proposed")),
    )

    result = adapter_server.form_embedded_module_handler_write_apply(
        {"old": "Line_x;", "new": "Line_y;"},
        base_id="upo_test",
        table="ConfigCASSave",
        file_name="form.0",
        handler_name="",
        mode="plan",
        timeout_seconds=30,
        method_name="metadata.write",
    )

    assert result["status"] == "ambiguous"
    assert result["error"] == "ambiguous_fragment"
    assert result["counts"]["occurrences"] == 2


def test_form_embedded_module_apply_replaces_fragment_inside_routine_scope(monkeypatch: pytest.MonkeyPatch) -> None:
    original_text = '{"meta","data","\r\n&НаКлиенте\r\nПроцедура Cmd(Команда)\r\n\t// same\r\nКонецПроцедуры\r\n\r\nПроцедура Other(Команда)\r\n\t// same\r\nКонецПроцедуры\r\n"}'
    original = compress_payload(original_text.encode("utf-8-sig"), "raw_deflate")
    seen: dict[str, Any] = {}

    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda *args, **kwargs: (original, {"database": "upo_test"}, None),
    )

    def fake_changes_propose(payload: dict[str, Any]) -> dict[str, Any]:
        seen["new_text"] = payload["edits"][0]["value"]
        expected_text, _info = patch_brace_text_path(
            decode_payload_lossless(original)["text"],
            "2",
            payload["edits"][0]["value"],
        )
        encoded = encode_payload_lossless(decode_payload_lossless(original), text=expected_text)
        return {
            "schema": "onec_change_proposal.v1",
            "status": "accepted_for_review",
            "source": {"table": "ConfigCASSave", "file_name": "form.0"},
            "original": {"sha1": hashlib.sha1(original).hexdigest(), "bytes": len(original)},
            "encoded": {"sha1": hashlib.sha1(encoded).hexdigest(), "bytes": len(encoded), "payload_hex": encoded.hex()},
            "edits": [{"mode": "path_preserve_format", "path": "2"}],
        }

    monkeypatch.setattr(adapter_server, "changes_propose", fake_changes_propose)
    monkeypatch.setattr(
        adapter_server,
        "storage_saved_state_apply_proposal",
        lambda payload: {"schema": "onec_storage_saved_state_apply.v1", "status": "applied", "applied": True, "readback": {"verified": True}},
    )

    result = adapter_server.form_embedded_module_handler_write_apply(
        {
            "old": "// same",
            "new": "// changed",
            "allow_sql_saved_state_apply": True,
        },
        base_id="upo_test",
        table="ConfigCASSave",
        file_name="form.0",
        handler_name="Cmd",
        mode="apply",
        timeout_seconds=30,
        method_name="metadata.write",
    )

    assert result["status"] == "applied"
    assert seen["new_text"].count("// changed") == 1
    assert seen["new_text"].count("// same") == 1
    assert "Процедура Other" in seen["new_text"]


def test_code_write_apply_requires_exact_verified_target(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_metadata_write(payload: dict[str, Any]) -> dict[str, Any]:
        seen["payload"] = payload
        return {
            "schema": "onec_metadata_write.v1",
            "method": "metadata.write",
            "status": "applied",
            "execution_mode": "apply",
            "target_kind": "module",
            "base_id": "upo_test",
            "routed_method": "form_embedded_module_handler_write_apply",
            "result": {
                "status": "applied",
                "applied": True,
                "source": {"table": "ConfigCASSave", "file_name": "form.0", "module_path": "2"},
            },
        }

    monkeypatch.setattr(adapter_server, "metadata_write", fake_metadata_write)

    result = adapter_server.code_write(
        {
            "base_id": "upo_test",
            "execution_mode": "apply",
            "object_type": "CommonForm",
            "object_name": "t_Форма",
            "routine_name": "ЗаменаДомена",
            "routine_text": "Процедура ЗаменаДомена(Команда)\nКонецПроцедуры\n",
        }
    )

    assert result["status"] == "invalid_argument"
    assert result["argument"] == "write_target"
    assert "payload" not in seen


def test_code_write_defaults_to_plan_not_apply(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}
    monkeypatch.setattr(
        adapter_server,
        "metadata_write",
        lambda payload: (seen.update(payload) or {"status": "planned", "result": {"status": "planned", "applied": False}}),
    )

    result = adapter_server.code_write(
        {
            "base_id": "upo_test",
            "ref": "Report.Тест",
            "old": "Старое",
            "new": "Новое",
        }
    )

    assert result["status"] == "planned"
    assert result["execution_mode"] == "plan"
    assert seen["mode"] == "plan"


def test_code_write_normalizes_public_owner_ref_before_routing(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_metadata_write(payload: dict[str, Any]) -> dict[str, Any]:
        seen["payload"] = payload
        return {
            "schema": "onec_metadata_write.v1",
            "method": "metadata.write",
            "status": "planned",
            "execution_mode": "plan",
            "target_kind": "module",
            "base_id": payload["base_id"],
            "routed_method": "metadata.module.write_apply",
            "result": {"status": "planned", "applied": False},
        }

    monkeypatch.setattr(adapter_server, "metadata_write", fake_metadata_write)

    result = adapter_server.call_method(
        "code.write",
        {
            "base_id": "upo_test",
            "ref": "Обработки.ОбменДанными",
            "routine_name": "ВыполнитьОбмен",
            "routine_text": "Процедура ВыполнитьОбмен()\nКонецПроцедуры\n",
            "execution_mode": "plan",
        },
    )

    assert result["status"] == "planned"
    assert seen["payload"]["target"] == {
        "kind": "module",
        "object_type": "DataProcessor",
        "object_name": "ОбменДанными",
        "routine_name": "ВыполнитьОбмен",
    }
    assert result["target"]["object_type"] == "DataProcessor"
    assert result["target"]["object_name"] == "ОбменДанными"
    assert result["target"]["routine_name"] == "ВыполнитьОбмен"


def test_code_write_help_exposes_name_first_owner_and_write_guards() -> None:
    method = {
        row["name"]: row
        for row in adapter_server.call_method("help.methods", {})["methods"]
    }["code.write"]

    assert method["selector_capabilities"]["accepts_ordinal"] is False
    assert method["selector_capabilities"]["accepts_module_ref"] is True
    assert method["selector_capabilities"]["form_selector_arguments"] == ["form", "form_name"]
    assert method["selector_capabilities"]["child_selector_arguments"] == ["routine_name"]
    assert method["selector_capabilities"]["write_guards"] == [
        "save_first",
        "allow_sql_saved_state_apply",
        "repository_apply_gate",
        "verified_saved_state_target",
    ]
    assert method["input_schema"]["properties"]["ref"]["type"] == "string"
    assert method["input_schema"]["properties"]["object_name"]["type"] == "string"


def test_code_write_records_write_history(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"server": "sql.example", "database": base_id}, None))

    monkeypatch.setattr(
        adapter_server,
        "metadata_write",
        lambda payload: {
            "schema": "onec_metadata_write.v1",
            "method": "metadata.write",
            "status": "applied",
            "execution_mode": "apply",
            "target_kind": "module",
            "base_id": payload["base_id"],
            "routed_method": "metadata.module.write_apply",
            "result": {"status": "applied", "applied": True, "backup": {"backup_id": "abcdefabcdefabcdefabcdefabcdefab"}},
        },
    )

    write_result = adapter_server.call_method(
        "code.write",
        {
            "base_id": "upo_test",
            "write_target": {
                "path": "Расширение.test2.ОбщиеФормы.t_Форма.МодульФормы",
                "expected_origin": {"extension": "test2"},
                "expected_sha1": "a" * 40,
            },
            "object_type": "CommonForm",
            "object_name": "t_Форма",
            "old": "Вертикальная",
            "new": "Горизонтальная",
        },
    )

    assert write_result["operation_id"]
    history = adapter_server.call_method("metadata.write.history", {"base_id": "upo_test", "operation_id": write_result["operation_id"]})
    assert history["counts"]["operations"] == 1
    operation = history["operations"][0]
    assert operation["method"] == "code.write"
    assert operation["status"] == "applied"
    assert operation["routed_method"] == "metadata.module.write_apply"
    assert operation["backup_ids"] == ["abcdefabcdefabcdefabcdefabcdefab"]


def test_code_write_rewrites_extension_first_common_form_path(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_metadata_write(payload: dict[str, Any]) -> dict[str, Any]:
        seen["payload"] = payload
        return {"status": "planned", "target": payload.get("target"), "result": {"applied": False}}

    monkeypatch.setattr(adapter_server, "metadata_write", fake_metadata_write)

    result = adapter_server.code_write(
        {
            "base_id": "upo_test",
            "extension": "test2",
            "path": "test2.t_Форма.ЗаменаДомена",
            "routine_text": "Процедура ЗаменаДомена(Команда)\nКонецПроцедуры\n",
            "mode": "plan",
        }
    )

    assert result["status"] == "planned"
    assert seen["payload"]["target"]["path"] == "CommonForm.t_Форма.ЗаменаДомена"
    assert seen["payload"]["target"]["object_type"] == "CommonForm"
    assert seen["payload"]["target"]["object_name"] == "t_Форма"
    assert seen["payload"]["routine_name"] == "ЗаменаДомена"


def test_code_write_facade_exposes_ambiguous_fragment_reason(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_write",
        lambda payload: {
            "schema": "onec_metadata_write.v1",
            "method": "metadata.write",
            "status": "ambiguous",
            "execution_mode": "plan",
            "target_kind": "module",
            "base_id": "upo_test",
            "routed_method": "form_embedded_module_handler_write_apply",
            "result": {
                "status": "ambiguous",
                "error": "ambiguous_fragment",
                "counts": {"occurrences": 3},
                "scope": {"kind": "routine", "routine_name": "ЗаменаДомена"},
                "diagnostics": {"message": "Fragment replacement requires old to occur exactly once."},
            },
        },
    )

    result = adapter_server.code_write(
        {
            "base_id": "upo_test",
            "target": {"canonical_path": "ОбщаяФорма.t_Форма.ЗаменаДомена"},
            "old": "\n",
            "new": "\n",
            "mode": "plan",
        }
    )

    assert result["status"] == "ambiguous"
    assert result["error"] == "ambiguous_fragment"
    assert result["counts"] == {"occurrences": 3}
    assert result["scope"] == {"kind": "routine", "routine_name": "ЗаменаДомена"}


def test_code_write_apply_does_not_discover_target_before_repository_gate(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_write",
        lambda payload: {
            "schema": "onec_metadata_write.v1",
            "method": "metadata.write",
            "status": "blocked",
            "result": {
                "status": "blocked",
                "applied": False,
                "apply_result": {
                    "status": "blocked",
                    "error": "repository_lock_required",
                    "diagnostics": {"message": "Capture the object before writing."},
                    "next_call": {
                        "method": "repository.lock.request",
                        "params": {"base_id": payload["base_id"]},
                    },
                },
            },
        },
    )

    result = adapter_server.code_write(
        {
            "base_id": "upo_test",
            "object_type": "CommonModule",
            "object_name": "ТестовыйМодуль",
            "module_text": "Процедура Тест()\nКонецПроцедуры\n",
        }
    )

    assert result["status"] == "blocked"
    assert result["error"] == "repository_lock_required"


def test_collect_backup_ids_ignores_untyped_operation_and_index_ids() -> None:
    backup_id = "0123456789abcdef0123456789abcdef"
    assert adapter_server.collect_backup_ids(
        {
            "backup": {"backup_id": backup_id},
            "operation_id": "11111111111111111111111111111111",
            "index_event": {"event_id": "22222222222222222222222222222222"},
        }
    ) == [backup_id]


def test_code_write_facade_exposes_success_fragment_scope(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_metadata_write(payload: dict[str, Any]) -> dict[str, Any]:
        seen["payload"] = payload
        return {
            "schema": "onec_metadata_write.v1",
            "method": "metadata.write",
            "status": "planned",
            "execution_mode": "plan",
            "target_kind": "module",
            "base_id": "upo_test",
            "routed_method": "form_embedded_module_handler_write_apply",
            "result": {
                "status": "planned",
                "routine": {"status": "fragment_replaced", "operation": "fragment_replace", "scope": "routine", "occurrences": 1},
            },
        }

    monkeypatch.setattr(
        adapter_server,
        "metadata_write",
        fake_metadata_write,
    )

    result = adapter_server.code_write(
        {
            "base_id": "upo_test",
            "target": {"canonical_path": "ОбщаяФорма.t_Форма.ЗаменаДомена"},
            "old": "// old",
            "new": "// new",
            "mode": "plan",
        }
    )

    assert result["status"] == "planned"
    assert result["scope"] == {"kind": "routine", "routine_name": "ЗаменаДомена"}
    assert result["counts"] == {"occurrences": 1}
    assert seen["payload"]["routine_name"] == "ЗаменаДомена"


def test_metadata_write_blocks_concrete_module_when_plan_has_problems(monkeypatch: pytest.MonkeyPatch) -> None:
    def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
        raise AssertionError("metadata.write must not call apply when write plan is blocked")

    monkeypatch.setattr(adapter_server, "metadata_module_write_apply", fake_apply)

    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "target": {
                "canonical_path": "ОбщийМодуль.Интеграция.Отправить",
                "module_ref": "ConfigCASSave:common_module.0#stream:0",
            },
            "intent": {"operation": "replace_with_control", "new": "Сообщить(\"new\");"},
        }
    )

    assert result["status"] == "blocked"
    assert result["error"] == "write_plan_blocked"
    assert result["plan"]["allowed"] is False
    assert any(problem["code"] == "missing_control_fragment" for problem in result["problems"])


def test_metadata_write_blocks_mismatched_concrete_reference_before_apply(monkeypatch: pytest.MonkeyPatch) -> None:
    def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
        raise AssertionError("metadata.write must not call apply for mismatched concrete reference")

    monkeypatch.setattr(adapter_server, "metadata_module_write_apply", fake_apply)

    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "target": {
                "kind": "module",
                "canonical_path": "ОбщийМодуль.Интеграция.Отправить",
                "form_guid": "form-guid",
            },
            "old": "a",
            "new": "b",
        }
    )

    assert result["status"] == "blocked"
    assert result["error"] == "write_plan_blocked"
    assert any(problem["code"] == "concrete_reference_kind_mismatch" for problem in result["problems"])


def test_code_symbol_resolve_full_path_uses_metadata_lookup(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_definition_find",
        lambda payload: {
            "schema": "onec_definition_find.v1",
            "status": "ok",
            "matches": [
                {
                    "canonical_path": "Справочник.Номенклатура.Артикул",
                    "kind": "Catalog",
                    "name": "Номенклатура",
                }
            ],
        },
    )

    result = call_method(
        "code.symbol.resolve",
        {
            "base_id": "base",
            "expression": "Справочник.Номенклатура.Артикул",
            "module_ref": "ConfigSave:file:0",
        },
    )

    assert result["status"] == "resolved"
    assert result["resolution_kind"] == "metadata_path"
    assert result["canonical_path"] == "Справочник.Номенклатура.Артикул"
    assert result["safe_as_metadata_path"] is True


def test_code_symbol_resolve_parameter_is_not_metadata(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "read_module",
        lambda payload: {
            "schema": "onec_module_read.v1",
            "status": "ok",
            "text": "Процедура ПередЗаписью(Отказ) Экспорт\n    Отказ = Истина;\nКонецПроцедуры",
            "owner": {"kind": "Catalog", "name": "Номенклатура"},
            "module": {"name": "Модуль объекта"},
        },
    )

    result = call_method(
        "code.symbol.resolve",
        {
            "base_id": "base",
            "expression": "Отказ.Код",
            "routine_name": "ПередЗаписью",
            "module_ref": "ConfigSave:file:0",
        },
    )

    assert result["status"] == "resolved"
    assert result["resolution_kind"] == "parameter"
    assert result["context_path"] == "Отказ.Код"
    assert result["safe_as_metadata_path"] is False


def test_code_symbol_resolve_short_object_name_stays_unsafe(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "read_module",
        lambda payload: {
            "schema": "onec_module_read.v1",
            "status": "ok",
            "text": "Процедура ПередЗаписью(Отказ) Экспорт\nКонецПроцедуры",
            "owner": {"kind": "Catalog", "name": "Номенклатура"},
            "module": {"name": "Модуль объекта"},
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_attributes",
        lambda payload: {"schema": "onec_metadata_object_attributes.v1", "status": "ok", "attributes": []},
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_definition_find",
        lambda payload: {
            "schema": "onec_definition_find.v1",
            "status": "ok",
            "matches": [{"canonical_path": "Справочник.Номенклатура", "kind": "Catalog", "name": "Номенклатура"}],
        },
    )

    result = call_method(
        "code.symbol.resolve",
        {
            "base_id": "base",
            "expression": "Номенклатура.ЕдИзмерение.Код",
            "routine_name": "ПередЗаписью",
            "module_ref": "ConfigSave:file:0",
        },
    )

    assert result["status"] == "unresolved"
    assert result["safe_as_metadata_path"] is False
    assert result["candidates"][0]["canonical_path"] == "Справочник.Номенклатура"
    assert result["candidates"][0]["reason"] == "short_object_name_requires_kind"


def test_module_origin_from_storage_table_marks_base_configuration() -> None:
    origin = adapter_server.module_origin_from_storage_table("Config")

    assert origin["source"] == "configuration"
    assert origin["storage_table"] == "Config"
    assert origin["write_surface"] == "base_saved_state"


def test_module_origin_from_storage_table_marks_saved_state() -> None:
    origin = adapter_server.module_origin_from_storage_table("ConfigSave")

    assert origin["source"] == "saved_state"
    assert origin["storage_table"] == "ConfigSave"
    assert origin["write_surface"] == "base_saved_state"


def test_module_origin_from_storage_table_requires_owner_for_configcas() -> None:
    origin = adapter_server.module_origin_from_storage_table("ConfigCAS")

    assert origin["source"] == "cas_reference"
    assert origin["status"] == "owner_unresolved"
    assert origin["write_surface"] == "requires_owner_resolution"


def test_code_read_preserves_module_origin(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "read_module",
        lambda payload: {
            "schema": "onec_module_read.v1",
            "status": "ok",
            "text": "Процедура Проверка()\nКонецПроцедуры",
            "origin": adapter_server.module_origin_from_storage_table("ConfigSave"),
        },
    )

    result = adapter_server.code_read({"base_id": "upo_test", "module_ref": "ConfigSave:object-module"})

    assert result["schema"] == "onec_code_read.v1"
    assert result["source"]["kind"] == "code_read"
    assert result["origin"]["source"] == "saved_state"
    assert result["origin"]["write_surface"] == "base_saved_state"


def test_form_write_matrix_build_routes_readable_title_to_effective_source(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_form_decode",
        lambda payload: {
            "status": "ok",
            "source": {"table": payload["table"], "file_name": payload["file_name"]},
            "form": {"file_name": payload["file_name"]},
            "profile": {
                "items": [
                    {
                        "name": "А",
                        "id": "3",
                        "title": "",
                        "path": "1.25.24.24.24",
                        "title_path": "1.25.24.24.24.4.2.1",
                        "path_to_data": "А",
                    }
                ],
                "attributes": [
                    {"name": "А", "id": "2", "title": "А", "path": "3.3", "title_path": "3.3.4.2.1"}
                ],
            },
        },
    )

    result = adapter_server.metadata_form_write_matrix_build(
        {"base_id": "upo_test", "table": "ConfigCASSave", "file_name": "form-guid.0"}
    )

    assert result["status"] == "ok"
    routed = [
        entry
        for entry in result["entries"]
        if entry["requested_target"]["section"] == "items"
        and entry["requested_target"]["name"] == "А"
        and entry["property"]["canonical_property"] == "title"
    ][0]
    assert routed["can_smoke"] is True
    assert routed["effective_target"]["section"] == "attributes"
    assert routed["effective_source"]["kind"] == "data_path_form_attribute_title"
    assert routed["property"]["write_path"] == "3.3.4.2.1"


def test_form_write_matrix_build_passes_owner_scope_only_to_file_resolution(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}
    owner_guid = "22222222-2222-2222-2222-222222222222"

    def fake_resolve_file(payload: dict[str, Any], **kwargs: Any) -> tuple[str, str]:
        seen["resolve_payload"] = payload
        return "ConfigCASSave", "resolved-form.0"

    def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
        seen["decode_payload"] = payload
        return {
            "status": "ok",
            "source": {"table": payload["table"], "file_name": payload["file_name"]},
            "form": {"name": "ФормаОбмена", "file_name": payload["file_name"]},
            "profile": {"items": [], "commands": [], "attributes": [], "tables": [], "command_bars": []},
        }

    monkeypatch.setattr(adapter_server, "write_learning_resolve_file", fake_resolve_file)
    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda *args, **kwargs: (None, {}, {"status": "not_found"}),
    )

    result = adapter_server.call_method(
        "metadata.form.write_matrix.build",
        {
            "base_id": "upo_test",
            "ref": "Обработки.ВторойОбмен",
            "object_guid": owner_guid,
            "form_name": "ФормаОбмена",
        },
    )

    assert result["status"] == "ok"
    assert seen["resolve_payload"]["kind"] == "DataProcessor"
    assert seen["resolve_payload"]["name"] == "ВторойОбмен"
    assert seen["resolve_payload"]["guid"] == owner_guid
    assert seen["resolve_payload"]["form_name"] == "ФормаОбмена"
    assert seen["decode_payload"]["file_name"] == "resolved-form.0"
    for key in ("ref", "kind", "name", "guid", "object_type", "object_name", "object_guid"):
        assert key not in seen["decode_payload"]


def test_form_target_move_and_write_matrix_help_expose_name_first_hierarchy() -> None:
    methods = {
        row["name"]: row
        for row in adapter_server.call_method("help.methods", {})["methods"]
    }

    move = methods["metadata.form.target.move"]
    assert move["selector_capabilities"]["form_selector_arguments"] == ["form", "form_name", "form_guid"]
    assert move["selector_capabilities"]["child_selector_arguments"] == [
        "from_element",
        "to_element",
        "with_element",
        "after_element",
    ]
    assert "allow_sql_saved_state_apply" in move["selector_capabilities"]["write_guards"]

    matrix_build = methods["metadata.form.write_matrix.build"]
    matrix_smoke = methods["metadata.form.write_matrix.smoke"]
    assert matrix_build["selector_capabilities"]["read_only"] is True
    assert matrix_smoke["selector_capabilities"]["read_only"] is False
    assert matrix_smoke["selector_capabilities"]["write_guards"] == [
        "allow_sql_saved_state_apply",
        "allow_sql_saved_state_rollback",
        "apply_and_rollback_only",
    ]
    for method in (matrix_build, matrix_smoke):
        assert method["selector_capabilities"]["accepts_ordinal"] is False
        assert method["selector_capabilities"]["form_selector_arguments"] == ["form", "form_name", "form_guid"]
        assert method["input_schema"]["properties"]["object_guid"]["type"] == "string"
        assert method["input_schema"]["properties"]["form_guid"]["type"] == "string"


def test_form_write_matrix_smoke_runs_safe_candidates_with_rollback(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_form_write_matrix_build",
        lambda payload: {
            "status": "ok",
            "source": {"table": "ConfigCASSave", "file_name": "form-guid.0"},
            "form": {"file_name": "form-guid.0"},
            "entries": [
                {
                    "can_smoke": True,
                    "selector": {"element_path": "5.3", "command": "КомандаПример1"},
                    "property": {"canonical_property": "title", "test_value": "SMOKE"},
                },
                {
                    "can_smoke": False,
                    "reason": "identity_or_binding_property",
                    "selector": {"element_path": "5.3"},
                    "property": {"canonical_property": "name"},
                },
            ],
            "counts": {"entries": 2},
        },
    )
    seen: dict[str, Any] = {}

    def fake_metadata_write(payload: dict[str, Any]) -> dict[str, Any]:
        seen["payload"] = payload
        return {
            "status": "verified_and_rolled_back",
            "result": {
                "rolled_back": True,
                "apply_result": {"semantic_verification": {"status": "ok", "checks": [{"ok": True}]}},
            },
        }

    monkeypatch.setattr(adapter_server, "metadata_write", fake_metadata_write)

    result = adapter_server.metadata_form_write_matrix_smoke(
        {
            "base_id": "upo_test",
            "table": "ConfigCASSave",
            "file_name": "form-guid.0",
            "allow_sql_saved_state_apply": True,
            "allow_sql_saved_state_rollback": True,
        }
    )

    assert result["status"] == "ok"
    assert result["counts"]["smoked"] == 1
    assert result["counts"]["verified"] == 1
    assert seen["payload"]["mode"] == "apply_and_rollback"
    assert seen["payload"]["target"]["element_path"] == "5.3"
    assert seen["payload"]["edits"] == [{"property": "title", "value": "SMOKE"}]


def test_write_learning_capture_uses_owner_scope_only_for_form_resolution(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    seen: dict[str, Any] = {}
    owner_guid = "22222222-2222-2222-2222-222222222222"
    monkeypatch.setenv("ONEC_ADAPTER_WRITE_LEARNING_DIR", str(tmp_path))

    def fake_resolve_file(payload: dict[str, Any], **kwargs: Any) -> tuple[str, str]:
        seen["resolve_payload"] = payload
        return "ConfigCASSave", "resolved-form.0"

    def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
        seen["decode_payload"] = payload
        return {
            "status": "ok",
            "source": {"table": payload["table"], "file_name": payload["file_name"]},
            "form": {"name": "ФормаОбмена", "file_name": payload["file_name"]},
            "profile": {"items": [], "commands": [], "attributes": [], "tables": [], "command_bars": []},
        }

    monkeypatch.setattr(adapter_server, "write_learning_resolve_file", fake_resolve_file)
    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda *args, **kwargs: (b"saved-form", {"server": "test", "database": "upo_test"}, None),
    )

    result = adapter_server.call_method(
        "metadata.write_learning.capture_before",
        {
            "base_id": "upo_test",
            "learning_id": "owner-form-scope",
            "ref": "Обработки.ВторойОбмен",
            "object_guid": owner_guid,
            "form_name": "ФормаОбмена",
            "element_name": "КнопкаВыполнить",
        },
    )

    assert result["status"] == "ok"
    assert Path(result["path"]).is_file()
    assert seen["resolve_payload"]["kind"] == "DataProcessor"
    assert seen["resolve_payload"]["name"] == "ВторойОбмен"
    assert seen["resolve_payload"]["guid"] == owner_guid
    assert seen["resolve_payload"]["form_name"] == "ФормаОбмена"
    for key in ("ref", "kind", "name", "guid", "object_type", "object_name", "object_guid"):
        assert key not in seen["decode_payload"]


def test_write_learning_capture_help_exposes_owner_and_form_selectors() -> None:
    methods = {
        row["name"]: row
        for row in adapter_server.call_method("help.methods", {})["methods"]
    }

    for method_name in ("metadata.write_learning.capture_before", "metadata.write_learning.capture_after"):
        method = methods[method_name]
        assert method["selector_capabilities"]["accepts_ordinal"] is False
        assert method["selector_capabilities"]["form_selector_arguments"] == ["form", "form_name", "form_guid"]
        assert method["selector_capabilities"]["write_guards"] == ["local_artifact_only", "no_sql_write"]
        assert method["input_schema"]["properties"]["object_guid"]["type"] == "string"
        assert method["input_schema"]["properties"]["form_guid"]["type"] == "string"


def test_write_learning_diff_and_infer_rule(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_WRITE_LEARNING_DIR", str(tmp_path))
    learning_id = "case1"
    root = tmp_path / learning_id
    root.mkdir()
    before_path = root / "before-b.json"
    after_path = root / "after-a.json"
    base_capture = {
        "schema": "onec_write_learning_capture.v1",
        "status": "ok",
        "learning_id": learning_id,
        "base_id": "upo_test",
        "source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": "form-guid.0"},
        "form": {"file_name": "form-guid.0"},
        "storage": {"sha1": "old", "bytes": 10},
        "targets": [
            {
                "section": "commands",
                "name": "КомандаПример1",
                "path": "5.3",
                "writable_properties": [
                    {
                        "property": "Заголовок",
                        "canonical_property": "title",
                        "presentation": "Заголовок",
                        "path": "5.3.3.2.1",
                        "value": "Пример1",
                    }
                ],
            }
        ],
    }
    before = {**base_capture, "snapshot_id": "b", "stage": "before"}
    after = json.loads(json.dumps({**base_capture, "snapshot_id": "a", "stage": "after", "storage": {"sha1": "new", "bytes": 12}}, ensure_ascii=False))
    after["targets"][0]["writable_properties"][0]["value"] = "ПРОВЕРКА"
    before_path.write_text(json.dumps(before, ensure_ascii=False), encoding="utf-8")
    after_path.write_text(json.dumps(after, ensure_ascii=False), encoding="utf-8")
    (root / "latest-before.json").write_text(json.dumps({"snapshot_id": "b", "path": str(before_path)}, ensure_ascii=False), encoding="utf-8")
    (root / "latest-after.json").write_text(json.dumps({"snapshot_id": "a", "path": str(after_path)}, ensure_ascii=False), encoding="utf-8")

    diff = adapter_server.metadata_write_learning_diff({"learning_id": learning_id})

    assert diff["status"] == "changed"
    assert diff["counts"]["changes"] == 1
    assert diff["changes"][0]["old"] == "Пример1"
    assert diff["changes"][0]["new"] == "ПРОВЕРКА"

    rule = adapter_server.metadata_write_learning_infer_rule({"learning_id": learning_id})

    assert rule["status"] == "ok"
    assert rule["metadata_write"] == {
        "method": "metadata.write",
        "payload": {
            "base_id": "upo_test",
            "target": {"kind": "form", "table": "ConfigCASSave", "file_name": "form-guid.0", "element_path": "5.3"},
            "mode": "plan",
            "edits": [{"property": "title", "value": "ПРОВЕРКА", "expected_old": "Пример1"}],
        },
    }

    public_rule = adapter_server.call_method("metadata.write_learning.infer_rule", {"learning_id": learning_id})
    assert public_rule["metadata_write"]["payload"]["target"]["file_name"] == "form-guid.0"
    assert public_rule["metadata_write"]["payload"]["target"]["element_path"] == "5.3"


def test_write_learning_capture_stores_decoded_targets(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_WRITE_LEARNING_DIR", str(tmp_path))

    def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "status": "ok",
            "source": {"kind": "live_sql", "table": payload["table"], "file_name": payload["file_name"]},
            "form": {"file_name": payload["file_name"]},
            "profile": {
                "commands": [
                    {
                        "name": "КомандаПример1",
                        "title": "Пример1",
                        "path": "5.3",
                        "title_path": "5.3.3.2.1",
                    }
                ]
            },
        }

    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda base_id, table, file_name, timeout_seconds=30: (b"payload", {"server": "s", "database": base_id}, None),
    )

    result = adapter_server.metadata_write_learning_capture(
        {
            "base_id": "upo_test",
            "learning_id": "case2",
            "table": "ConfigCASSave",
            "file_name": "form-guid.0",
        },
        "before",
    )

    assert result["status"] == "ok"
    path = Path(result["path"])
    assert path.exists()
    capture = json.loads(path.read_text(encoding="utf-8"))
    assert capture["storage"]["sha1"] == adapter_server.hashlib.sha1(b"payload").hexdigest()
    assert "payload_hex" not in json.dumps(capture, ensure_ascii=False)
    assert capture["targets"][0]["writable_properties"][0]["path"] == "5.3.3.2.1"


def test_saved_state_backups_list_filters_without_payload_hex(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    backup_dir = tmp_path / "backups"
    backup_dir.mkdir()
    (backup_dir / "20260625T010101Z-a.json").write_text(
        json.dumps(
            {
                "schema": "onec_storage_apply_backup.v1",
                "backup_id": "a",
                "created_at_utc": "2026-06-25T01:01:01Z",
                "base_id": "upo_test",
                "source": {"database": "upo_test", "table": "ConfigCASSave", "file_name": "form.0"},
                "original": {"sha1": "old", "bytes": 10, "payload_hex": "ffff"},
                "replacement": {"sha1": "new", "bytes": 11},
            },
            ensure_ascii=False,
        ),
        encoding="utf-8",
    )
    monkeypatch.setenv("ONEC_ADAPTER_BACKUP_DIR", str(backup_dir))

    result = adapter_server.storage_saved_state_backups_list({"base_id": "upo_test", "table": "ConfigCASSave", "file_name": "form.0"})

    assert result["status"] == "ok"
    assert result["counts"]["returned"] == 1
    assert result["backups"][0]["backup_id"] == "a"
    assert "payload_hex" not in result["backups"][0]["original"]


def test_saved_state_backups_prune_defaults_to_dry_run_and_preserves_latest(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    backup_dir = tmp_path / "backups"
    backup_dir.mkdir()
    now = time.time()
    paths = []
    for index, age_days in enumerate((90, 60, 1), start=1):
        path = backup_dir / f"backup-{index}.json"
        path.write_text(
            json.dumps(
                {
                    "schema": "onec_storage_apply_backup.v1",
                    "backup_id": f"{index:032x}",
                    "created_at_utc": "2026-01-01T00:00:00Z",
                    "base_id": "upo_test",
                    "source": {
                        "table": "ConfigCASSave",
                        "file_name": "form.0",
                    },
                }
            ),
            encoding="utf-8",
        )
        modified_at = now - age_days * 86400
        os.utime(path, (modified_at, modified_at))
        paths.append(path)
    monkeypatch.setenv("ONEC_ADAPTER_BACKUP_DIR", str(backup_dir))
    monkeypatch.setattr(
        adapter_server,
        "metadata_write_history_referenced_backup_ids",
        lambda base_id: {f"{1:032x}"} if base_id == "upo_test" else set(),
    )

    dry_run = adapter_server.storage_saved_state_backups_prune(
        {
            "base_id": "upo_test",
            "table": "ConfigCASSave",
            "older_than_days": 30,
            "keep_latest": 1,
        }
    )

    assert dry_run["status"] == "ok"
    assert dry_run["dry_run"] is True
    assert dry_run["counts"]["selected"] == 1
    assert dry_run["counts"]["deleted"] == 0
    assert dry_run["counts"]["protected_by_write_history"] == 1
    assert all(path.exists() for path in paths)

    confirmation = adapter_server.storage_saved_state_backups_prune(
        {
            "base_id": "upo_test",
            "older_than_days": 30,
            "keep_latest": 1,
            "dry_run": False,
        }
    )
    assert confirmation["status"] == "confirmation_required"

    applied = adapter_server.storage_saved_state_backups_prune(
        {
            "base_id": "upo_test",
            "older_than_days": 30,
            "keep_latest": 1,
            "dry_run": False,
            "confirm_delete": True,
        }
    )
    assert applied["status"] == "ok"
    assert applied["counts"]["deleted"] == 1
    assert sum(path.exists() for path in paths) == 2
    assert paths[0].exists()


def test_form_write_target_resolve_finds_command_property(monkeypatch: pytest.MonkeyPatch) -> None:
    def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "status": "ok",
            "source": {"kind": "live_sql", "table": payload["table"], "file_name": payload["file_name"]},
            "form": {"file_name": payload["file_name"]},
            "profile": {
                "items": [],
                "commands": [
                    {
                        "name": "КомандаПример1",
                        "id": "2",
                        "title": "Пример1",
                        "path": "5.3",
                        "title_path": "5.3.3.2.1",
                        "parameters": [{"index": 3, "presentation": "Заголовок", "value": "Пример1"}],
                    }
                ],
            },
        }

    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)

    result = adapter_server.metadata_form_write_target_resolve(
        {
            "base_id": "upo_test",
            "table": "ConfigCASSave",
            "file_name": "form-guid.0",
            "command": "КомандаПример1",
            "property": "Заголовок",
            "value": "ПРОВЕРКА",
        }
    )

    assert result["status"] == "ok"
    assert result["target"]["section"] == "commands"
    assert result["property"] == {"property": "Заголовок", "path": "5.3.3.2.1", "old": "Пример1", "status": "ok", "new": "ПРОВЕРКА"}
    assert result["semantic_diff"]["old"] == "Пример1"
    assert result["semantic_diff"]["new"] == "ПРОВЕРКА"


def test_saved_state_forms_search_indexes_form_commands(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {
            "status": "ok",
            "files": [{"FileName": "extension__form.0", "PartCount": 1, "Bytes": 123}],
            "counts": {"files": 1},
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_form_decode",
        lambda payload: {
            "status": "ok",
            "source": {"kind": "live_sql", "table": payload["table"], "file_name": payload["file_name"]},
            "form": {"file_name": payload["file_name"]},
            "counts": {"commands": 1},
            "profile": {"items": [], "commands": [{"name": "КомандаПример1", "title": "Пример1", "path": "5.3", "title_path": "5.3.3.2.1"}]},
        },
    )

    result = adapter_server.metadata_saved_state_forms_search(
        {
            "base_id": "upo_test",
            "tables": ["ConfigCASSave"],
            "element": "КомандаПример1",
            "include_storage": True,
        }
    )

    assert result["status"] == "ok"
    assert result["counts"]["forms"] == 1
    assert result["forms"][0]["file_name"] == "extension__form.0"
    assert result["forms"][0]["source"]["file_name"] == "extension__form.0"
    assert result["forms"][0]["form"]["file_name"] == "extension__form.0"
    assert result["forms"][0]["matches"][0]["section"] == "commands"
    assert result["forms"][0]["matches"][0]["name"] == "КомандаПример1"



def test_saved_state_forms_search_scans_base_form_rows_without_suffix(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: list[str] = []

    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {
            "status": "ok",
            "files": [{"FileName": "base-form-guid", "PartCount": 1, "Bytes": 123}],
            "counts": {"files": 1},
        },
    )

    def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
        seen.append(payload["file_name"])
        return {
            "status": "ok",
            "source": {"kind": "live_sql", "table": payload["table"], "file_name": payload["file_name"]},
            "form": {"file_name": payload["file_name"]},
            "profile": {"items": [{"name": "Наименование", "title": "Наименование", "path": "1"}], "commands": []},
        }

    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)

    result = adapter_server.metadata_saved_state_forms_search(
        {"base_id": "upo_test", "tables": ["ConfigSave"], "include_storage": True}
    )

    assert seen == ["base-form-guid"]
    assert result["counts"]["forms"] == 1
    assert result["forms"][0]["file_name"] == "base-form-guid"

def test_form_element_write_auto_resolves_file_name(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_resolve(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "status": "ok",
            "source": {"table": "ConfigCASSave", "file_name": "form-guid.0"},
            "target": {"section": "commands", "name": "КомандаПример1", "path": "5.3"},
            "property": {"status": "ok", "path": "5.3.3.2.1", "old": "Пример1", "new": "ПРОВЕРКА"},
        }

    def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
        seen["decode"] = payload
        return {
            "status": "ok",
            "source": {"kind": "live_sql", "table": payload["table"], "file_name": payload["file_name"]},
            "form": {"file_name": payload["file_name"]},
            "profile": {
                "commands": [
                    {"name": "КомандаПример1", "title": "Пример1", "path": "5.3", "title_path": "5.3.3.2.1"}
                ]
            },
        }

    def fake_changes(payload: dict[str, Any]) -> dict[str, Any]:
        seen["changes"] = payload
        return {"status": "accepted_for_review", "source": payload["source"], "edits": payload["edits"]}

    monkeypatch.setattr(adapter_server, "metadata_form_write_target_resolve", fake_resolve)
    monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
    monkeypatch.setattr(adapter_server, "changes_propose", fake_changes)

    result = adapter_server.metadata_form_element_write(
        {
            "base_id": "upo_test",
            "table": "ConfigCASSave",
            "element": "КомандаПример1",
            "allow_saved_state_write": True,
            "edits": [{"property": "Заголовок", "value": "ПРОВЕРКА"}],
        }
    )

    assert seen["decode"]["file_name"] == "form-guid.0"
    assert "element_path" not in seen["decode"]
    assert seen["changes"]["source"]["file_name"] == "form-guid.0"
    assert result["semantic_diff"][0]["presentation"] == "КомандаПример1.Заголовок: Пример1 -> ПРОВЕРКА"


def test_objects_list_rejects_ambiguous_query_argument() -> None:
    result = adapter_server.validate_adapter_job_payload("metadata.objects.list", {"base_id": "upo_test", "query": "Документ"})

    assert result is not None
    assert result["status"] == "invalid_argument"
    assert result["argument"] == "query"


def test_form_element_write_apply_plan_returns_proposal(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_plan(payload: dict[str, Any]) -> dict[str, Any]:
        seen["payload"] = payload
        return {"status": "accepted_for_review", "source": {"table": "ConfigSave", "file_name": "form-guid.0"}}

    monkeypatch.setattr(adapter_server, "metadata_form_element_write", fake_plan)

    result = adapter_server.metadata_form_element_write_apply(
        {
            "base_id": "upo_test",
            "execution_mode": "plan",
            "table": "ConfigSave",
            "file_name": "form-guid.0",
            "element_id": "67",
            "edits": [{"property": "title", "value": "Новый"}],
        }
    )

    assert seen["payload"]["allow_saved_state_write"] is True
    assert "include_payload" not in seen["payload"]
    assert result["status"] == "planned"
    assert result["proposal"]["status"] == "accepted_for_review"


def test_form_element_write_apply_smoke_applies_and_rolls_back(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}
    proposal = {
        "status": "accepted_for_review",
        "source": {"table": "ConfigSave", "file_name": "form-guid.0"},
        "encoded": {"payload_hex": "00"},
    }

    def fake_plan(payload: dict[str, Any]) -> dict[str, Any]:
        seen["plan"] = payload
        return proposal

    def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
        seen["apply"] = payload
        return {"status": "applied", "applied": True, "backup": {"backup_id": "b" * 32}, "semantic_verification": {"status": "ok"}}

    def fake_rollback(payload: dict[str, Any]) -> dict[str, Any]:
        seen["rollback"] = payload
        return {"status": "applied", "applied": True}

    monkeypatch.setattr(adapter_server, "metadata_form_element_write", fake_plan)
    monkeypatch.setattr(adapter_server, "storage_saved_state_apply_proposal", fake_apply)
    monkeypatch.setattr(adapter_server, "storage_saved_state_rollback", fake_rollback)

    result = adapter_server.metadata_form_element_write_apply(
        {
            "base_id": "upo_test",
            "execution_mode": "apply_and_rollback",
            "allow_sql_saved_state_apply": True,
            "allow_sql_saved_state_rollback": True,
            "table": "ConfigSave",
            "file_name": "form-guid.0",
            "element_id": "67",
            "edits": [{"property": "title", "value": "Новый"}],
            "timeout_seconds": 60,
        }
    )

    assert seen["plan"]["allow_saved_state_write"] is True
    assert seen["plan"]["include_payload"] is True
    assert seen["apply"]["proposal"] is proposal
    assert seen["rollback"]["backup_id"] == "b" * 32
    assert result["status"] == "verified_and_rolled_back"
    assert result["applied"] is True
    assert result["rolled_back"] is True


def test_form_element_write_apply_and_verify_does_not_rollback(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}
    proposal = {
        "status": "accepted_for_review",
        "source": {"table": "ConfigSave", "file_name": "form-guid.0"},
        "encoded": {"payload_hex": "00"},
    }

    def fake_plan(payload: dict[str, Any]) -> dict[str, Any]:
        seen["plan"] = payload
        return proposal

    def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
        seen["apply"] = payload
        return {
            "status": "applied",
            "applied": True,
            "readback": {"verified": True},
            "semantic_verification": {"status": "ok"},
        }

    def fake_rollback(payload: dict[str, Any]) -> dict[str, Any]:
        seen["rollback"] = payload
        return {"status": "applied", "applied": True}

    monkeypatch.setattr(adapter_server, "metadata_form_element_write", fake_plan)
    monkeypatch.setattr(adapter_server, "storage_saved_state_apply_proposal", fake_apply)
    monkeypatch.setattr(adapter_server, "storage_saved_state_rollback", fake_rollback)

    result = adapter_server.metadata_form_element_write_apply(
        {
            "base_id": "upo_test",
            "execution_mode": "apply_and_verify",
            "allow_sql_saved_state_apply": True,
            "table": "ConfigSave",
            "file_name": "form-guid.0",
            "element_id": "67",
            "edits": [{"property": "title", "value": "Новый"}],
        }
    )

    assert seen["plan"]["include_payload"] is True
    assert seen["apply"]["proposal"] is proposal
    assert "rollback" not in seen
    assert result["status"] == "verified"
    assert result["applied"] is True


def test_form_element_write_apply_and_rollback_preserves_failed_apply_status(monkeypatch: pytest.MonkeyPatch) -> None:
    proposal = {"status": "accepted_for_review", "source": {"table": "ConfigSave", "file_name": "form-guid.0"}, "encoded": {"payload_hex": "00"}}
    monkeypatch.setattr(adapter_server, "metadata_form_element_write", lambda payload: proposal)
    monkeypatch.setattr(adapter_server, "storage_saved_state_apply_proposal", lambda payload: {
        "status": "blocked", "applied": False, "diagnostics": {"message": "apply gate rejected"},
    })

    result = adapter_server.metadata_form_element_write_apply({
        "base_id": "upo_test", "execution_mode": "apply_and_rollback",
        "allow_sql_saved_state_apply": True, "allow_sql_saved_state_rollback": True,
        "table": "ConfigSave", "file_name": "form-guid.0",
        "edits": [{"property": "title", "value": "Новый"}],
    })

    assert result["status"] == "blocked"
    assert result["applied"] is False
    assert result["diagnostics"] == {"message": "apply gate rejected"}


def test_form_element_write_apply_forwards_resolved_extension_context(monkeypatch: pytest.MonkeyPatch) -> None:
    proposal = {
        "status": "accepted_for_review",
        "source": {"table": "ConfigCASSave", "file_name": "extension-guid__form-guid.0"},
        "encoded": {"payload_hex": "00"},
    }
    seen: dict[str, Any] = {}

    def fake_context(payload: dict[str, Any]) -> dict[str, Any]:
        result = dict(payload)
        result["extension_guid"] = "a" * 32
        result["development_layer_resolution"] = {"status": "resolved", "layer_id": f"extension:{'a' * 32}"}
        return result

    monkeypatch.setattr(adapter_server, "resolve_write_gate_context", fake_context)
    monkeypatch.setattr(adapter_server, "repository_apply_gate", lambda *args: None)
    monkeypatch.setattr(adapter_server, "metadata_form_element_write", lambda payload: proposal)
    def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
        seen["apply"] = payload
        return {"status": "applied", "applied": True}

    monkeypatch.setattr(adapter_server, "storage_saved_state_apply_proposal", fake_apply)

    adapter_server.metadata_form_element_write_apply({
        "base_id": "upo_test", "extension": "test2", "execution_mode": "apply",
        "allow_sql_saved_state_apply": True,
        "edits": [{"property": "title", "value": "Новый"}],
    })

    assert seen["apply"]["extension_guid"] == "a" * 32
    assert seen["apply"]["development_layer_resolution"]["layer_id"] == f"extension:{'a' * 32}"
    assert seen["apply"]["module_ref"] == "ConfigCASSave:extension-guid__form-guid.0#form_module"


def test_form_element_write_apply_response_hides_payload_hex(monkeypatch: pytest.MonkeyPatch) -> None:
    proposal = {
        "status": "accepted_for_review",
        "source": {"table": "ConfigSave", "file_name": "form-guid.0"},
        "encoded": {"payload_hex": "00", "sha1": "new"},
    }
    seen: dict[str, Any] = {}

    monkeypatch.setattr(adapter_server, "metadata_form_element_write", lambda payload: proposal)

    def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
        seen["proposal"] = payload["proposal"]
        return {"status": "applied", "applied": True, "readback": {"verified": True}, "semantic_verification": {"status": "ok"}}

    monkeypatch.setattr(adapter_server, "storage_saved_state_apply_proposal", fake_apply)

    result = adapter_server.metadata_form_element_write_apply(
        {
            "base_id": "upo_test",
            "execution_mode": "apply_and_verify",
            "allow_sql_saved_state_apply": True,
            "table": "ConfigSave",
            "file_name": "form-guid.0",
            "element_id": "67",
            "edits": [{"property": "title", "value": "Новый"}],
        }
    )

    assert seen["proposal"]["encoded"]["payload_hex"] == "00"
    assert "payload_hex" not in result["proposal"]["encoded"]


def test_adapter_service_token_uses_constant_time_bearer_check(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_SERVICE_TOKEN", "secret-token")

    assert adapter_server.adapter_request_authorized("Bearer secret-token") is True
    assert adapter_server.adapter_request_authorized("bearer secret-token") is True
    assert adapter_server.adapter_request_authorized("Bearer wrong") is False
    assert adapter_server.adapter_request_authorized(None) is False


def test_query_validation_blocks_unsafe_read_features_but_ignores_literals() -> None:
    blocked = adapter_server.validate_query(
        {"base_id": "upo_test", "query": "SELECT * FROM OPENROWSET(BULK 'x', SINGLE_BLOB) AS payload"}
    )
    literal = adapter_server.validate_query(
        {"base_id": "upo_test", "query": "SELECT N'update and openrowset are plain text' AS message"}
    )

    assert blocked["valid"] is False
    assert blocked["reason"] == "unsafe_read_feature"
    assert literal["valid"] is True


def test_sensitive_query_result_fields_are_masked() -> None:
    rows, fields = adapter_server.mask_sensitive_query_rows(
        [{"email": "test@example.com", "ИНН": "123", "Наименование": "Тест", "phone": None}]
    )

    assert rows == [{"email": "***", "ИНН": "***", "Наименование": "Тест", "phone": None}]
    assert fields == ["email", "phone", "ИНН"]


def test_data_schema_resolves_public_object_and_logical_fields(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "get_object",
        lambda *args, **kwargs: {
            "status": "ok",
            "object": {
                "kind": "Catalog",
                "name": "Контрагенты",
                "guid": "a" * 32,
                "storage": {"dbnames": [{"sql_number": 42}]},
            },
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "data_sql_rows",
        lambda *args, **kwargs: (
            [
                {"name": "_IDRRef", "type_name": "binary", "max_length": 16, "precision": 0, "scale": 0, "is_nullable": False},
                {"name": "_Fld100", "type_name": "nvarchar", "max_length": 100, "precision": 0, "scale": 0, "is_nullable": True},
            ],
            None,
        ),
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_attributes",
        lambda payload: {
            "status": "ok",
            "attributes": [
                {
                    "name": "ИНН",
                    "type": {"kind": "string", "length": 12},
                    "storage_routes": [{"physical_name_candidate": "_Fld100"}],
                }
            ],
        },
    )

    result = adapter_server.data_object_schema({"base_id": "upo_test", "kind": "Catalog", "name": "Контрагенты"})

    assert result["status"] == "ok"
    assert result["table"]["name"] == "_Reference42"
    assert [field["name"] for field in result["fields"]] == ["ref", "ИНН"]


def test_data_schema_resolves_platform_85_constant_table_and_value(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "get_object",
        lambda *args, **kwargs: {
            "status": "ok",
            "object": {
                "kind": "Constant",
                "name": "ИспользоватьСотрудников",
                "guid": "a" * 32,
                "storage": {"dbnames": [{"storage_role": "Const", "sql_number": 56995}]},
            },
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "data_sql_rows",
        lambda *args, **kwargs: (
            [
                {"name": "_Fld56996", "type_name": "binary", "max_length": 1, "precision": 0, "scale": 0, "is_nullable": False},
                {"name": "_RecordKey", "type_name": "binary", "max_length": 1, "precision": 0, "scale": 0, "is_nullable": False},
            ],
            None,
        ),
    )
    monkeypatch.setattr(adapter_server, "metadata_object_attributes", lambda payload: {"status": "ok"})

    result = adapter_server.data_object_schema(
        {"base_id": "upo_test", "kind": "Constant", "name": "ИспользоватьСотрудников"}
    )

    assert result["status"] == "ok"
    assert result["table"]["name"] == "_Const56995"
    assert [field["name"] for field in result["fields"]] == ["value", "record_key"]
    assert result["fields"][0]["section"] == "value"


def test_data_schema_has_physical_enum_table_route() -> None:
    assert adapter_server.DATA_TABLE_PREFIXES["Enum"] == "_Enum"


def test_data_schema_has_physical_business_process_table_route() -> None:
    assert adapter_server.DATA_TABLE_PREFIXES["BusinessProcess"] == "_BPr"


def test_data_schema_maps_business_process_and_task_system_columns() -> None:
    assert adapter_server.DATA_SYSTEM_COLUMNS["_Completed"] == "completed"
    assert adapter_server.DATA_SYSTEM_COLUMNS["_Started"] == "started"
    assert adapter_server.DATA_SYSTEM_COLUMNS["_HeadTaskRRef"] == "head_task_ref"
    assert adapter_server.DATA_SYSTEM_COLUMNS["_BusinessProcess_RRRef"] == "business_process"
    assert adapter_server.DATA_SYSTEM_COLUMNS["_Point_RRRef"] == "route_point"
    assert adapter_server.DATA_SYSTEM_COLUMNS["_Executed"] == "executed"


def test_data_schema_cache_key_unifies_equivalent_public_selectors() -> None:
    by_ref = adapter_server.data_schema_cache_key(
        {"base_id": "upo_test", "ref": "ChartOfCalculationTypes.Начисления"}
    )
    enriched = adapter_server.data_schema_cache_key(
        {
            "base_id": "UPO_TEST",
            "ref": "ChartOfCalculationTypes.Начисления",
            "kind": "ChartOfCalculationTypes",
            "name": "Начисления",
            "guid": "be08c831-b5a8-4e28-b13c-26875635a7f1",
        }
    )
    by_kind_name = adapter_server.data_schema_cache_key(
        {"base_id": "upo_test", "kind": "ChartOfCalculationTypes", "name": "Начисления"}
    )

    assert by_ref == enriched == by_kind_name


@pytest.mark.parametrize("logical_name", ["completed", "started", "executed"])
def test_data_decoder_treats_workflow_flags_as_booleans(logical_name: str) -> None:
    assert adapter_server.onec_data_value(b"\x01", logical_name=logical_name) is True
    assert adapter_server.onec_data_value(b"\x00", logical_name=logical_name) is False


def test_data_schema_selector_keeps_public_ref_with_explicit_record_ref() -> None:
    payload = adapter_server.data_schema_selector_payload(
        {"base_id": "upo_test", "ref": "Enum.Статусы", "record_ref": "01" * 16}
    )
    assert payload["ref"] == "Enum.Статусы"
    assert "record_ref" not in payload


def test_decode_data_rows_uses_constant_boolean_type() -> None:
    rows = adapter_server.decode_data_rows(
        [{"value": b"\x01"}],
        {"value": "value"},
        {"value": {"kind": "boolean", "presentation": "Булево"}},
    )
    assert rows == [{"value": True}]


def test_enrich_enum_data_rows_uses_guid_variant() -> None:
    guid = "a18b60a1-81f1-4852-a612-0510aa3159b1"
    rows = adapter_server.enrich_enum_data_rows(
        [{"ref": {"type": "reference", "guid_variants": ["a6120510-aa31-59b1-4852-81f1a18b60a1", guid]}}],
        {guid: {"name": "АсинхронныйОбмен", "synonym": "Прямое соединение", "value_ref": "Enum.ПрограммыБанка.EnumValue.АсинхронныйОбмен"}},
    )
    assert rows[0]["name"] == "АсинхронныйОбмен"
    assert rows[0]["ref"] == {"type": "reference", "guid_variants": ["a6120510-aa31-59b1-4852-81f1a18b60a1", guid]}
    assert rows[0]["value_ref"] == "Enum.ПрограммыБанка.EnumValue.АсинхронныйОбмен"


def test_data_read_builds_parameterized_logical_query_and_decodes_values(monkeypatch: pytest.MonkeyPatch) -> None:
    schema = {
        "status": "ok",
        "base_id": "upo_test",
        "object": {"kind": "Catalog", "name": "Контрагенты"},
        "table": {"name": "_Reference42"},
        "fields": [
            {"name": "ref", "physical_name": "_IDRRef"},
            {"name": "description", "physical_name": "_Description"},
            {"name": "marked_for_deletion", "physical_name": "_Marked"},
        ],
    }
    seen: dict[str, Any] = {}
    monkeypatch.setattr(adapter_server, "data_object_schema", lambda payload: schema)

    def fake_rows(base_id: str, query: str, params: Any = None, **kwargs: Any) -> tuple[list[dict[str, Any]], None]:
        seen.update({"base_id": base_id, "query": query, "params": params})
        return [{"ref": bytes.fromhex("01" * 16), "description": "Тест", "marked_for_deletion": b"\x00"}], None

    monkeypatch.setattr(adapter_server, "data_sql_rows", fake_rows)
    result = adapter_server.data_read(
        {"base_id": "upo_test", "kind": "Catalog", "name": "Контрагенты", "filters": {"description": "Тест"}, "limit": 10}
    )

    assert result["status"] == "ok"
    assert "[_Description]=%s" in seen["query"]
    assert "[_Marked]=0x00" in seen["query"]
    assert seen["params"][0] == "Тест"
    assert result["rows"][0]["description"] == "Тест"
    assert result["rows"][0]["marked_for_deletion"] is False


def test_data_list_keeps_public_object_ref_separate_from_record_ref(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}
    schema = {
        "status": "ok",
        "base_id": "upo_test",
        "object": {"kind": "Catalog", "name": "Контрагенты"},
        "table": {"name": "_Reference42"},
        "fields": [{"name": "ref", "physical_name": "_IDRRef"}],
    }

    def fake_schema(payload: dict[str, Any]) -> dict[str, Any]:
        seen["schema_payload"] = payload
        return schema

    def fake_rows(base_id: str, query: str, params: Any = None, **kwargs: Any) -> tuple[list[dict[str, Any]], None]:
        seen.update({"query": query, "params": params})
        return [], None

    monkeypatch.setattr(adapter_server, "data_object_schema", fake_schema)
    monkeypatch.setattr(adapter_server, "data_sql_rows", fake_rows)

    result = adapter_server.data_read({"base_id": "upo_test", "ref": "Catalog.Контрагенты", "limit": 10}, method="data.list")

    assert result["status"] == "ok"
    assert seen["schema_payload"]["ref"] == "Catalog.Контрагенты"
    assert "[_IDRRef]=%s" not in seen["query"]


def test_data_get_uses_record_ref_and_object_ref_independently(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}
    schema = {
        "status": "ok",
        "base_id": "upo_test",
        "object": {"kind": "Catalog", "name": "Контрагенты"},
        "table": {"name": "_Reference42"},
        "fields": [{"name": "ref", "physical_name": "_IDRRef"}],
    }
    monkeypatch.setattr(adapter_server, "data_object_schema", lambda payload: seen.setdefault("schema_payload", payload) and schema)

    def fake_rows(base_id: str, query: str, params: Any = None, **kwargs: Any) -> tuple[list[dict[str, Any]], None]:
        seen.update({"query": query, "params": params})
        return [], None

    monkeypatch.setattr(adapter_server, "data_sql_rows", fake_rows)
    record_ref = "01" * 16

    result = adapter_server.data_read(
        {"base_id": "upo_test", "object_ref": "Catalog.Контрагенты", "record_ref": record_ref, "limit": 1},
        method="data.get",
    )

    assert result["status"] == "ok"
    assert seen["schema_payload"]["ref"] == "Catalog.Контрагенты"
    assert "[_IDRRef]=%s" in seen["query"]
    assert seen["params"][0] == bytes.fromhex(record_ref)


def test_data_virtual_accumulation_turnovers_use_signed_resources(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}
    schema = {
        "status": "ok",
        "base_id": "upo_test",
        "object": {"kind": "AccumulationRegister", "name": "ОстаткиТоваров"},
        "table": {"name": "_AccumRg10"},
        "fields": [
            {"name": "period", "physical_name": "_Period", "section": "system"},
            {"name": "active", "physical_name": "_Active", "section": "system"},
            {"name": "_RecordKind", "physical_name": "_RecordKind", "section": "system"},
            {"name": "Номенклатура", "physical_name": "_Fld11RRef", "section": "dimensions"},
            {"name": "Количество", "physical_name": "_Fld12", "section": "resources"},
        ],
    }
    monkeypatch.setattr(adapter_server, "data_object_schema", lambda payload: schema)

    def fake_rows(base_id: str, query: str, params: Any = None, **kwargs: Any) -> tuple[list[dict[str, Any]], None]:
        seen.update({"query": query, "params": params})
        return [{"Номенклатура": bytes.fromhex("01" * 16), "Количество": 3.5}], None

    monkeypatch.setattr(adapter_server, "data_sql_rows", fake_rows)
    result = adapter_server.data_virtual(
        {
            "base_id": "upo_test",
            "kind": "AccumulationRegister",
            "name": "ОстаткиТоваров",
            "virtual_table": "turnovers",
            "start": "2025-01-01",
            "end": "2025-01-31",
            "allow_full_scan": True,
        }
    )

    assert result["status"] == "ok"
    assert "CASE WHEN [_RecordKind]=0" in seen["query"]
    assert "GROUP BY [_Fld11RRef]" in seen["query"]
    assert seen["params"][0].year == 4025
    assert seen["params"][1].year == 4025
    assert result["rows"][0]["Количество"] == 3.5


def test_data_virtual_information_slice_uses_dimensions_and_period(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}
    schema = {
        "status": "ok",
        "base_id": "upo_test",
        "object": {"kind": "InformationRegister", "name": "Цены"},
        "table": {"name": "_InfoRg20"},
        "fields": [
            {"name": "period", "physical_name": "_Period", "section": "system"},
            {"name": "Номенклатура", "physical_name": "_Fld21RRef", "section": "dimensions"},
            {"name": "Цена", "physical_name": "_Fld22", "section": "resources"},
        ],
    }
    monkeypatch.setattr(adapter_server, "data_object_schema", lambda payload: schema)

    def fake_rows(base_id: str, query: str, params: Any = None, **kwargs: Any) -> tuple[list[dict[str, Any]], None]:
        seen.update({"query": query, "params": params})
        return [], None

    monkeypatch.setattr(adapter_server, "data_sql_rows", fake_rows)
    result = adapter_server.data_virtual(
        {"base_id": "upo_test", "kind": "InformationRegister", "name": "Цены", "virtual_table": "СрезПоследних", "period": "2025-02-01", "allow_full_scan": True}
    )

    assert result["status"] == "ok"
    assert "ROW_NUMBER() OVER (PARTITION BY [_Fld21RRef] ORDER BY [_Period] DESC)" in seen["query"]
    assert "[_Period]<=%s" in seen["query"]
    assert seen["params"][0].year == 4025


def test_metadata_module_write_apply_plans_stream_edit(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_changes_propose(payload: dict[str, Any]) -> dict[str, Any]:
        seen["payload"] = payload
        return {
            "status": "accepted_for_review",
            "source": {"table": "ConfigCASSave", "file_name": "object__module.0", "stream_index": 4},
            "encoded": {"sha1": "new"},
        }

    monkeypatch.setattr(adapter_server, "changes_propose", fake_changes_propose)
    monkeypatch.setattr(adapter_server, "metadata_module_write_stream_codec_guard", lambda **kwargs: None)

    result = adapter_server.metadata_module_write_apply(
        {
            "base_id": "upo_test",
            "module_ref": "ConfigCASSave:object__module.0#stream:4",
            "allow_saved_state_write": True,
            "old": "Перем Параметры;     ",
            "new": "Перем Параметры;      ",
            "expected_contains": "Перем Параметры;",
            "expected_text_sha1": "a" * 40,
        }
    )

    assert result["status"] == "planned"
    assert result["module_ref"] == "ConfigCASSave:object__module.0#stream:4"
    assert seen["payload"]["source"]["table"] == "ConfigCASSave"
    assert seen["payload"]["source"]["file_name"] == "object__module.0"
    assert seen["payload"]["source"]["module_id"] == "ConfigCASSave:object__module.0#stream:4"
    assert seen["payload"]["edits"] == [
        {
            "stream_index": 4,
            "expected_contains": "Перем Параметры;",
            "expected_text_sha1": "a" * 40,
            "replace": {"old": "Перем Параметры;     ", "new": "Перем Параметры;      "},
        }
    ]


def test_metadata_module_write_apply_applies_and_rolls_back(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}
    proposal = {
        "status": "accepted_for_review",
        "source": {"table": "ConfigCASSave", "file_name": "object__module.0"},
        "encoded": {"payload_hex": "00", "sha1": "new"},
    }

    def fake_changes_propose(payload: dict[str, Any]) -> dict[str, Any]:
        seen["proposal_payload"] = payload
        return proposal

    def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
        seen["apply"] = payload
        return {"status": "applied", "applied": True, "backup": {"backup_id": "c" * 32}, "readback": {"verified": True}}

    def fake_rollback(payload: dict[str, Any]) -> dict[str, Any]:
        seen["rollback"] = payload
        return {"status": "applied", "applied": True}

    monkeypatch.setattr(adapter_server, "changes_propose", fake_changes_propose)
    monkeypatch.setattr(adapter_server, "metadata_write_apply_plan_gate", lambda *args, **kwargs: ({"allowed": True}, None))
    monkeypatch.setattr(adapter_server, "repository_apply_gate", lambda *args, **kwargs: None)
    monkeypatch.setattr(adapter_server, "metadata_module_write_stream_codec_guard", lambda **kwargs: None)
    monkeypatch.setattr(adapter_server, "storage_saved_state_apply_proposal", fake_apply)
    monkeypatch.setattr(adapter_server, "storage_saved_state_rollback", fake_rollback)

    result = adapter_server.metadata_module_write_apply(
        {
            "base_id": "upo_test",
            "module_ref": "ConfigCASSave:object__module.0#stream:0",
            "allow_saved_state_write": True,
            "mode": "apply_and_rollback",
            "allow_sql_saved_state_apply": True,
            "allow_sql_saved_state_rollback": True,
            "lock_session_id": "rlock-test",
            "repository_object": "Справочник.Тест",
            "ref": "Catalog.Тест",
            "kind": "Catalog",
            "name": "Тест",
            "old": "a",
            "new": "b",
        }
    )

    assert seen["proposal_payload"]["include_payload"] is True
    assert seen["apply"]["proposal"] is proposal
    assert seen["apply"]["lock_session_id"] == "rlock-test"
    assert seen["apply"]["repository_object"] == "Справочник.Тест"
    assert seen["apply"]["ref"] == "Catalog.Тест"
    assert seen["apply"]["kind"] == "Catalog"
    assert seen["apply"]["name"] == "Тест"
    assert seen["rollback"]["backup_id"] == "c" * 32
    assert seen["rollback"]["lock_session_id"] == "rlock-test"
    assert seen["rollback"]["repository_object"] == "Справочник.Тест"
    assert seen["rollback"]["ref"] == "Catalog.Тест"
    assert result["proposal"]["encoded"] == {"sha1": "new"}
    assert result["status"] == "verified_and_rolled_back"
    assert result["applied"] is True
    assert result["rolled_back"] is True


def test_metadata_module_write_apply_accepts_flat_routine_edit(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}
    routine_text = "Функция Настроить(УсловноеОформление) Экспорт\n\tВозврат Неопределено;\nКонецФункции\n"

    def fake_changes_propose(payload: dict[str, Any]) -> dict[str, Any]:
        seen["payload"] = payload
        return {
            "status": "accepted_for_review",
            "source": {"table": "ConfigCASSave", "file_name": "object__module.0", "stream_index": 4},
            "encoded": {"sha1": "new"},
        }

    monkeypatch.setattr(adapter_server, "changes_propose", fake_changes_propose)
    monkeypatch.setattr(adapter_server, "metadata_module_write_stream_codec_guard", lambda **kwargs: None)

    result = adapter_server.metadata_module_write_apply(
        {
            "base_id": "upo_test",
            "module_ref": "ConfigCASSave:object__module.0#stream:4",
            "allow_saved_state_write": True,
            "routine_name": "Настроить",
            "routine_text": routine_text,
            "routine_operation": "replace",
            "expected_old_contains": "ТипУсловноеОформление",
        }
    )

    assert result["status"] == "planned"
    assert seen["payload"]["edits"] == [
        {
            "stream_index": 4,
            "routine": {
                "text": routine_text,
                "operation": "replace",
                "name": "Настроить",
                "expected_old_contains": "ТипУсловноеОформление",
            },
        }
    ]


def test_metadata_module_write_apply_scopes_fragment_to_routine(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}
    stream_text = (
        "&\u041d\u0430\u041a\u043b\u0438\u0435\u043d\u0442\u0435\r\n"
        "\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430 Cmd(\u041a\u043e\u043c\u0430\u043d\u0434\u0430)\r\n"
        "\t// same\r\n"
        "\u041a\u043e\u043d\u0435\u0446\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\r\n\r\n"
        "\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430 Other(\u041a\u043e\u043c\u0430\u043d\u0434\u0430)\r\n"
        "\t// same\r\n"
        "\u041a\u043e\u043d\u0435\u0446\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\r\n"
    )
    stream_bytes = stream_text.encode("utf-8-sig")
    header = f"\r\n{len(stream_bytes):08x} {len(stream_bytes):08x} 7fffffff \r\n".encode("ascii")
    stored = compress_payload(b"prefix" + header + stream_bytes, "raw_deflate")

    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda *args, **kwargs: (stored, {"database": "upo_test"}, None),
    )

    def fake_changes_propose(payload: dict[str, Any]) -> dict[str, Any]:
        seen["payload"] = payload
        return {
            "status": "accepted_for_review",
            "source": {"table": "ConfigCASSave", "file_name": "object__module.0", "stream_index": 0},
            "encoded": {"sha1": "new"},
        }

    monkeypatch.setattr(adapter_server, "changes_propose", fake_changes_propose)
    monkeypatch.setattr(adapter_server, "metadata_write_apply_plan_gate", lambda *args, **kwargs: ({"allowed": True}, None))

    result = adapter_server.metadata_module_write_apply(
        {
            "base_id": "upo_test",
            "module_ref": "ConfigCASSave:object__module.0#stream:0",
            "allow_saved_state_write": True,
            "_force_fragment_replace": True,
            "routine_name": "Cmd",
            "old": "// same",
            "new": "// changed",
        }
    )

    assert result["status"] == "planned"
    assert result["scope"] == {"kind": "routine", "routine_name": "Cmd"}
    assert result["counts"] == {"occurrences": 1}
    routine_edit = seen["payload"]["edits"][0]["routine"]
    assert routine_edit["name"] == "Cmd"
    assert routine_edit["operation"] == "replace"
    assert routine_edit["text"].count("// changed") == 1
    assert "// same" not in routine_edit["text"]


def test_metadata_module_write_apply_reports_routine_fragment_ambiguity(monkeypatch: pytest.MonkeyPatch) -> None:
    stream_text = (
        "\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430 Cmd(\u041a\u043e\u043c\u0430\u043d\u0434\u0430)\r\n"
        "\t// same\r\n"
        "\t// same\r\n"
        "\u041a\u043e\u043d\u0435\u0446\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\r\n"
    )
    stream_bytes = stream_text.encode("utf-8-sig")
    header = f"\r\n{len(stream_bytes):08x} {len(stream_bytes):08x} 7fffffff \r\n".encode("ascii")
    stored = compress_payload(b"prefix" + header + stream_bytes, "raw_deflate")

    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda *args, **kwargs: (stored, {"database": "upo_test"}, None),
    )
    monkeypatch.setattr(
        adapter_server,
        "changes_propose",
        lambda payload: (_ for _ in ()).throw(AssertionError("ambiguous routine fragment must not be proposed")),
    )
    monkeypatch.setattr(adapter_server, "metadata_write_apply_plan_gate", lambda *args, **kwargs: ({"allowed": True}, None))

    result = adapter_server.metadata_module_write_apply(
        {
            "base_id": "upo_test",
            "module_ref": "ConfigCASSave:object__module.0#stream:0",
            "allow_saved_state_write": True,
            "_force_fragment_replace": True,
            "routine_name": "Cmd",
            "old": "// same",
            "new": "// changed",
        }
    )

    assert result["status"] == "ambiguous"
    assert result["error"] == "ambiguous_fragment"
    assert result["counts"] == {"occurrences": 2}
    assert result["scope"] == {"kind": "routine", "routine_name": "Cmd"}


def test_saved_state_modules_search_returns_stream_refs(monkeypatch: pytest.MonkeyPatch) -> None:
    module_text = "Процедура Команда1(Команда)\r\nКонецПроцедуры\r\n".encode("utf-8-sig")
    header = f"\r\n{len(module_text):08x} {len(module_text):08x} 7fffffff \r\n".encode("ascii")
    stored = compress_payload(b"prefix" + header + module_text, "raw_deflate")
    file_name = "owner-guid__module-guid.0"

    def fake_files_list(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "status": "ok",
            "files": [{"FileName": file_name, "PartCount": 1, "Bytes": len(stored)}],
        }

    def fake_read_storage_file_bytes(base_id: str, table: str, requested_file_name: str, *, timeout_seconds: int = 30):
        assert base_id == "upo_test"
        assert table == "ConfigCASSave"
        assert requested_file_name == file_name
        return stored, {"database": "upo_test"}, None

    monkeypatch.setattr(adapter_server, "storage_files_list", fake_files_list)
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)

    result = adapter_server.metadata_saved_state_modules_search(
        {
            "base_id": "upo_test",
            "tables": ["ConfigCASSave"],
            "query": "Команда1",
            "limit": 10,
            "include_storage": True,
        }
    )

    assert result["status"] == "ok"
    assert result["counts"]["modules"] == 1
    module = result["modules"][0]
    assert module["identity"] == {"owner_guid": "owner-guid", "module_guid": "module-guid"}
    assert module["streams"][0]["module_ref"] == "ConfigCASSave:owner-guid__module-guid.0#stream:0"
    assert module["streams"][0]["match"]["in_text"] is True
    write_target = module["streams"][0]["write_plan_target"]
    assert write_target["kind"] == "module"
    assert write_target["module_ref"] == "ConfigCASSave:owner-guid__module-guid.0#stream:0"
    assert write_target["file_name"] == file_name
    assert write_target["stream_index"] == 0
    assert write_target["object_guid"] == "owner-guid"
    assert write_target["module_guid"] == "module-guid"
    assert write_target["expected_sha1"] == module["payload"]["sha1"]


def test_saved_state_modules_search_returns_form_embedded_module_container(monkeypatch: pytest.MonkeyPatch) -> None:
    form_text = '{"meta","data","&НаКлиенте\r\nПроцедура ЗаменаДомена(Команда)\r\nКонецПроцедуры\r\n\r\n///----\\",\r\n{4,0,0,0,{#base64:AAAA}}"}'
    stored = compress_payload(form_text.encode("utf-8-sig"), "raw_deflate")
    descriptor = compress_payload('{"t_Форма"}'.encode("utf-8-sig"), "raw_deflate")
    file_name = "extension-guid__form-guid.0"
    descriptor_file_name = "extension-guid__form-guid"

    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {
            "status": "ok",
            "files": [
                {"FileName": descriptor_file_name, "PartCount": 1, "Bytes": len(descriptor)},
                {"FileName": file_name, "PartCount": 1, "Bytes": len(stored)},
            ],
        },
    )

    def fake_read_storage_file_bytes(base_id: str, table: str, requested_file_name: str, *, timeout_seconds: int = 30):
        assert base_id == "upo_test"
        assert table == "ConfigCASSave"
        if requested_file_name == descriptor_file_name:
            return descriptor, {"database": "upo_test"}, None
        assert requested_file_name == file_name
        return stored, {"database": "upo_test"}, None

    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)

    result = adapter_server.metadata_saved_state_modules_search(
        {
            "base_id": "upo_test",
            "tables": ["ConfigCASSave"],
            "query": "ЗаменаДомена",
            "limit": 10,
            "include_storage": True,
        }
    )

    assert result["status"] == "ok"
    assert result["counts"]["modules"] == 1
    module = result["modules"][0]
    assert module["payload"]["role"] == "form_embedded_module_payload"
    assert module["form"]["name"] == "t_Форма"
    assert module["module"] == {"kind": "form_module", "name": "Модуль формы"}
    stream = module["streams"][0]
    assert stream["module_ref"] == "ConfigCASSave:extension-guid__form-guid.0#form_module"
    assert stream["module_path"] == "2"
    assert stream["form"]["name"] == "t_Форма"
    assert stream["module"] == {"kind": "form_module", "name": "Модуль формы"}
    assert "///----" not in stream["preview"]
    assert stream["match"]["in_text"] is True
    write_target = stream["write_plan_target"]
    assert write_target["kind"] == "module"
    assert write_target["module_ref"] == "ConfigCASSave:extension-guid__form-guid.0#form_module"
    assert "#stream:" not in write_target["module_ref"]
    assert write_target["file_name"] == file_name
    assert write_target["module_path"] == "2"
    assert write_target["object_guid"] == "extension-guid"
    assert write_target["form_guid"] == "form-guid"


def test_public_saved_state_module_search_row_is_name_first() -> None:
    public = adapter_server.public_saved_state_modules_search_row(
        {
            "table": "ConfigCASSave",
            "file_name": "extension-guid__form-guid.0",
            "identity": {"owner_guid": "extension-guid", "module_guid": "form-guid"},
            "owner": {
                "status": "resolved",
                "kind": "Catalog",
                "name": "test2",
                "synonym": "Test2",
                "guid": "owner-guid",
                "source": "saved_state_descriptor",
            },
            "form": {"name": "t_Форма", "guid": "form-guid", "source": "saved_state_descriptor"},
            "module": {"kind": "form_module", "name": "Модуль формы"},
            "qualified_name": "test2.t_Форма.Модуль формы",
            "payload": {"sha1": "payload-sha1"},
            "streams": [
                {
                    "module_ref": "ConfigCASSave:extension-guid__form-guid.0",
                    "module_path": "2",
                    "preview": "Процедура Команда()\nКонецПроцедуры",
                    "match": {"query": "Команда", "in_text": True, "in_file_name": False},
                    "write_plan_target": {
                        "module_ref": "ConfigCASSave:extension-guid__form-guid.0",
                        "file_name": "extension-guid__form-guid.0",
                    },
                }
            ],
        },
        requested_extension="test2",
    )

    assert public is not None
    assert public["qualified_name"] == "test2.t_Форма.Модуль формы"
    assert public["selector"] == {
        "extension": "test2",
        "ref": "Catalog.test2",
        "form": "t_Форма",
        "module": "Модуль формы",
        "qualified_name": "test2.t_Форма.Модуль формы",
    }
    assert public["streams"][0]["match"] == {"query": "Команда", "in_text": True, "in_name": False}
    assert public["streams"][0]["write_plan_target"] == {
        "kind": "module",
        "extension": "test2",
        "ref": "Catalog.test2",
        "form": "t_Форма",
        "module": "Модуль формы",
        "qualified_name": "test2.t_Форма.Модуль формы",
        "stream_ordinal": 1,
    }
    for storage_marker in (
        "ConfigCASSave",
        "extension-guid",
        "form-guid",
        "owner-guid",
        "module_ref",
        "file_name",
        "sha1",
    ):
        assert storage_marker not in str(public)


def test_modules_search_strips_form_embedded_module_tail(monkeypatch: pytest.MonkeyPatch) -> None:
    form_text = '{"meta","data","&НаКлиенте\r\nПроцедура ЗаменаДомена(Команда)\r\n\t// Отредактировано адаптером\r\nКонецПроцедуры\r\n\r\n///----\\",\r\n{4,0,0,0,{#base64:AAAA}}"}'
    stored = compress_payload(form_text.encode("utf-8-sig"), "raw_deflate")
    file_name = "extension-guid__form-guid.0"

    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {
            "status": "ok",
            "files": [{"FileName": file_name, "PartCount": 1, "Bytes": len(stored)}],
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_files_bytes",
        lambda base_id, table, file_names, *, timeout_seconds=60: ({file_name: stored}, {"database": "upo_test"}, None),
    )
    monkeypatch.setattr(adapter_server, "metadata_form_owner_cache_lookup", lambda *args, **kwargs: None)
    monkeypatch.setattr(adapter_server, "saved_state_public_module_context", lambda **kwargs: {})

    result = adapter_server.search_modules(
        {
            "base_id": "upo_test",
            "table": "ConfigCASSave",
            "query": "Отредактировано адаптером",
            "limit": 5,
        }
    )

    assert result["status"] == "ok"
    assert result["counts"]["matches"] == 1
    snippet = result["matches"][0]["snippet"]
    assert "Отредактировано адаптером" in snippet["text"]
    assert "///----" not in snippet["text"]


def test_saved_state_modules_search_scans_nonzero_base_module_parts(monkeypatch: pytest.MonkeyPatch) -> None:
    module_text = "Процедура Команда3(Команда)\r\nКонецПроцедуры\r\n".encode("utf-8-sig")
    header = f"\r\n{len(module_text):08x} {len(module_text):08x} 7fffffff \r\n".encode("ascii")
    stored = compress_payload(b"prefix" + header + module_text, "raw_deflate")
    file_name = "object-guid.3"

    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {
            "status": "ok",
            "files": [{"FileName": file_name, "PartCount": 1, "Bytes": len(stored)}],
        },
    )

    def fake_read_storage_file_bytes(base_id: str, table: str, requested_file_name: str, *, timeout_seconds: int = 30):
        assert base_id == "upo_test"
        assert table == "ConfigSave"
        assert requested_file_name == file_name
        return stored, {"database": "upo_test"}, None

    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)

    result = adapter_server.metadata_saved_state_modules_search(
        {
            "base_id": "upo_test",
            "tables": ["ConfigSave"],
            "query": "Команда3",
            "limit": 10,
            "include_storage": True,
        }
    )

    assert result["status"] == "ok"
    assert result["counts"]["modules"] == 1
    assert result["modules"][0]["file_name"] == file_name
    assert result["modules"][0]["streams"][0]["module_ref"] == "ConfigSave:object-guid.3#stream:0"


def test_saved_state_catalog_modules_report_owner_roles_and_repair_mojibake(monkeypatch: pytest.MonkeyPatch) -> None:
    good_text = "// Это модуль менеджера\r\n&НаКлиенте\r\nПроцедура ОбработкаКоманды(Команда)\r\nКонецПроцедуры\r\n"
    mojibake_text = good_text.encode("utf-8").decode("cp1251")
    module_text = mojibake_text.encode("utf-8-sig")
    header = f"\r\n{len(module_text):08x} {len(module_text):08x} 7fffffff \r\n".encode("ascii")
    stored = compress_payload(b"prefix" + header + module_text, "raw_deflate")
    file_name = "extension-guid__catalog-guid.3"

    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {
            "status": "ok",
            "files": [{"FileName": file_name, "PartCount": 1, "Bytes": len(stored)}],
        },
    )

    def fake_read_storage_file_bytes(base_id: str, table: str, requested_file_name: str, *, timeout_seconds: int = 30):
        assert requested_file_name == file_name
        return stored, {"database": "upo_test"}, None

    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)
    monkeypatch.setattr(
        adapter_server,
        "saved_state_descriptor_identity",
        lambda **kwargs: {
            "name": "tt_Справочник1",
            "synonym": "Tt справочник1",
            "guid": "catalog-guid",
            "source": "saved_state_descriptor",
        },
    )

    result = adapter_server.metadata_saved_state_modules_search(
        {
            "base_id": "upo_test",
            "tables": ["ConfigCASSave"],
            "object_type": "Catalog",
            "object_name": "tt_Справочник1",
            "limit": 10,
        }
    )

    assert result["status"] == "ok"
    module = result["modules"][0]
    assert module["owner"]["kind"] == "Catalog"
    assert module["owner"]["name"] == "tt_Справочник1"
    assert module["module"]["kind"] == "manager_module"
    stream = module["streams"][0]
    assert stream["module"]["kind"] == "manager_module"
    assert stream["owner"]["name"] == "tt_Справочник1"
    assert stream["encoding_repaired"] is True
    assert adapter_server.is_bsl_like_text(stream["preview"])


def test_saved_state_command_module_resolves_owner_from_related_descriptor(monkeypatch: pytest.MonkeyPatch) -> None:
    module_text = "&НаКлиенте\r\nПроцедура ОбработкаКоманды(Команда)\r\nКонецПроцедуры\r\n".encode("utf-8-sig")
    header = f"\r\n{len(module_text):08x} {len(module_text):08x} 7fffffff \r\n".encode("ascii")
    stored_module = compress_payload(b"prefix" + header + module_text, "raw_deflate")
    descriptor_text = '{"tt_Справочник1","command-guid"}'
    stored_descriptor = compress_payload(descriptor_text.encode("utf-8-sig"), "raw_deflate")
    module_file_name = "extension-guid__command-guid.2"
    descriptor_file_name = "extension-guid__catalog-guid"

    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {
            "status": "ok",
            "files": [
                {"FileName": descriptor_file_name, "PartCount": 1, "Bytes": len(stored_descriptor)},
                {"FileName": module_file_name, "PartCount": 1, "Bytes": len(stored_module)},
            ],
        },
    )

    def fake_read_storage_file_bytes(base_id: str, table: str, requested_file_name: str, *, timeout_seconds: int = 30):
        assert base_id == "upo_test"
        assert table == "ConfigCASSave"
        if requested_file_name == module_file_name:
            return stored_module, {"database": "upo_test"}, None
        if requested_file_name == descriptor_file_name:
            return stored_descriptor, {"database": "upo_test"}, None
        return None, {"database": "upo_test"}, {"status": "not_found"}

    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)

    result = adapter_server.metadata_saved_state_modules_search(
        {
            "base_id": "upo_test",
            "tables": ["ConfigCASSave"],
            "object_type": "Catalog",
            "object_name": "tt_Справочник1",
            "limit": 10,
        }
    )

    assert result["status"] == "ok"
    module = result["modules"][0]
    assert module["owner"]["kind"] == "Catalog"
    assert module["owner"]["name"] == "tt_Справочник1"
    assert module["owner"]["source"] == "saved_state_descriptor"
    assert module["module"]["kind"] == "command_module"
    stream = module["streams"][0]
    assert stream["owner"]["name"] == "tt_Справочник1"
    assert stream["module"]["kind"] == "command_module"


def test_saved_state_modules_search_resolves_object_name_to_owner_guid(monkeypatch: pytest.MonkeyPatch) -> None:
    module_text = "Процедура ПередЗаписью(Отказ)\r\nКонецПроцедуры\r\n".encode("utf-8-sig")
    header = f"\r\n{len(module_text):08x} {len(module_text):08x} 7fffffff \r\n".encode("ascii")
    stored = compress_payload(b"prefix" + header + module_text, "raw_deflate")
    file_name = "owner-guid__module-guid.0"
    seen: dict[str, Any] = {}

    monkeypatch.setattr(
        adapter_server,
        "metadata_cache_lookup_row",
        lambda base_id, kind, name: {
            "guid": "owner-guid",
            "kind": kind,
            "kind_ru": "Справочник",
            "public_kind": "catalog",
            "name": name,
            "source": "base",
        },
    )

    def fake_files_list(payload: dict[str, Any]) -> dict[str, Any]:
        seen.setdefault("files_payload", payload)
        return {"status": "ok", "files": [{"FileName": file_name, "PartCount": 1, "Bytes": len(stored)}]}

    def fake_read_storage_file_bytes(base_id: str, table: str, requested_file_name: str, *, timeout_seconds: int = 30):
        return stored, {"database": "upo_test"}, None

    monkeypatch.setattr(adapter_server, "storage_files_list", fake_files_list)
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)

    result = adapter_server.call_method(
        adapter_server.SAVED_STATE_MODULES_SEARCH_METHOD,
        {
            "base_id": "upo_test",
            "tables": ["ConfigCASSave"],
            "object_type": "Catalog",
                "object_name": "Номенклатура",
                "query": "ПередЗаписью",
                "limit": 10,
                "include_storage": True,
        },
    )

    assert result["status"] == "ok"
    assert seen["files_payload"]["prefix"] == "owner-guid"
    assert result["owner_resolution"]["status"] == "resolved"
    assert result["owner_resolution"]["owner_guid"] == "owner-guid"
    assert result["query"]["object_name"] == "Номенклатура"
    assert result["modules"][0]["streams"][0]["module_ref"] == "ConfigCASSave:owner-guid__module-guid.0#stream:0"
    assert result["modules"][0]["streams"][0]["write_plan_target"]["module_ref"] == "ConfigCASSave:owner-guid__module-guid.0#stream:0"


def test_saved_state_modules_search_normalizes_public_owner_ref(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_cache_lookup(base_id: str, kind: str, name: str) -> dict[str, Any]:
        seen["cache_lookup"] = {"base_id": base_id, "kind": kind, "name": name}
        return {
            "guid": "owner-guid",
            "kind": kind,
            "kind_ru": "Справочник",
            "public_kind": "catalog",
            "name": name,
            "source": "base",
        }

    def fake_files(payload: dict[str, Any]) -> dict[str, Any]:
        seen["files_payload"] = payload
        return {"status": "ok", "files": []}

    monkeypatch.setattr(adapter_server, "metadata_cache_lookup_row", fake_cache_lookup)
    monkeypatch.setattr(adapter_server, "storage_files_list", fake_files)

    result = adapter_server.call_method(
        adapter_server.SAVED_STATE_MODULES_SEARCH_METHOD,
        {
            "base_id": "upo_test",
            "ref": "Справочники.Номенклатура",
            "tables": ["ConfigCASSave"],
        },
    )

    assert result["status"] == "ok"
    assert seen["cache_lookup"] == {
        "base_id": "upo_test",
        "kind": "Catalog",
        "name": "Номенклатура",
    }
    assert seen["files_payload"]["prefix"] == "owner-guid"
    assert result["query"]["object_type"] == "Catalog"
    assert result["query"]["object_name"] == "Номенклатура"


def test_saved_state_modules_search_help_exposes_name_first_owner_selector() -> None:
    method = {
        row["name"]: row
        for row in adapter_server.call_method("help.methods", {})["methods"]
    }[adapter_server.SAVED_STATE_MODULES_SEARCH_METHOD]

    assert method["selector_capabilities"]["accepts_ordinal"] is False
    assert method["selector_capabilities"]["allows_global_search"] is True
    assert method["selector_capabilities"]["read_only"] is True
    assert method["selector_capabilities"]["module_selector_arguments"] == ["file_name", "stream_index"]
    assert method["input_schema"]["properties"]["ref"]["type"] == "string"
    assert method["input_schema"]["properties"]["object_name"]["type"] == "string"


def test_saved_state_modules_search_resolves_common_form_name_to_saved_file(monkeypatch: pytest.MonkeyPatch) -> None:
    form_text = '{"meta","data","&НаКлиенте\r\nПроцедура Cmd(Команда)\r\nКонецПроцедуры\r\n"}'
    stored = compress_payload(form_text.encode("utf-8-sig"), "raw_deflate")
    selected_file = "extension-guid__form-guid.0"
    other_file = "extension-guid__other-form-guid.0"
    seen: dict[str, Any] = {"read_files": []}

    monkeypatch.setattr(
        adapter_server,
        "metadata_cache_lookup_row",
        lambda base_id, kind, name: None,
    )
    monkeypatch.setattr(
        adapter_server,
        "list_objects",
        lambda *args, **kwargs: {"objects": []},
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_forms_search",
        lambda payload: {
            "status": "ok",
            "forms": [
                {"file_name": selected_file, "name": "TestForm"},
                {"file_name": other_file, "name": "TestFormCopy"},
            ],
            "counts": {"forms": 2},
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "storage_files_list",
        lambda payload: {
            "status": "ok",
            "files": [
                {"FileName": selected_file, "PartCount": 1, "Bytes": len(stored)},
                {"FileName": other_file, "PartCount": 1, "Bytes": len(stored)},
            ],
        },
    )

    def fake_read_storage_file_bytes(base_id: str, table: str, requested_file_name: str, *, timeout_seconds: int = 30):
        seen["read_files"].append(requested_file_name)
        assert requested_file_name == selected_file
        return stored, {"database": "upo_test"}, None

    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)

    result = adapter_server.metadata_saved_state_modules_search(
        {
            "base_id": "upo_test",
            "tables": ["ConfigCASSave"],
            "object_type": "CommonForm",
            "object_name": "TestForm",
            "query": "Cmd",
            "limit": 10,
            "include_storage": True,
        }
    )

    assert result["status"] == "ok"
    assert result["owner_resolution"]["status"] == "resolved"
    assert result["owner_resolution"]["method"] == "metadata.saved_state.forms.search"
    assert result["owner_resolution"]["file_names"] == [selected_file]
    assert seen["read_files"] == [selected_file, selected_file]
    assert result["counts"]["modules"] == 1
    assert result["modules"][0]["file_name"] == selected_file
    assert result["modules"][0]["payload"]["role"] == "form_embedded_module_payload"


def test_saved_state_prepare_plans_active_to_save_copy(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_file_names(payload: dict[str, Any], base_id: str, source_table: str, timeout_seconds: int):
        seen["source_table"] = source_table
        return ["owner__module.0"], {"guid": "owner", "kind": "CommonForm", "name": "t_Форма"}, None

    def fake_row_details(base_id: str, table: str, file_names: list[str], *, timeout_seconds: int = 30):
        if table == "ConfigCAS":
            return [
                {"FileName": "owner__module.0", "PartNo": 0, "DataSize": 10, "BinaryBytes": 10, "BinarySHA1": "A" * 40}
            ], {"database": "upo_test"}, None
        if table == "ConfigCASSave":
            return [], {"database": "upo_test"}, None
        raise AssertionError(table)

    monkeypatch.setattr(adapter_server, "saved_state_prepare_file_names", fake_file_names)
    monkeypatch.setattr(adapter_server, "saved_state_copy_row_details", fake_row_details)

    result = adapter_server.metadata_saved_state_prepare(
        {
            "base_id": "upo_test",
            "target_table": "ConfigCASSave",
            "object_type": "CommonForm",
            "object_name": "t_Форма",
            "include_storage": True,
        }
    )

    assert seen["source_table"] == "ConfigCAS"
    assert result["status"] == "plan_ready"
    assert result["ready_to_copy"] is True
    assert result["source"]["table"] == "ConfigCAS"
    assert result["target"]["table"] == "ConfigCASSave"
    assert result["counts"] == {"file_names": 1, "copy_file_names": 1, "source_rows": 1, "copy_source_rows": 1, "target_rows": 0}
    assert result["write_mode"]["sql_write_performed"] is False
    assert [stage["name"] for stage in result["stages"]] == [
        "resolve_source",
        "inspect_active_source",
        "inspect_saved_target",
    ]
    assert all(stage["status"] == "ok" for stage in result["stages"])


@pytest.mark.parametrize("module_suffix", [".0", ".2"])
def test_extension_prepare_route_maps_proven_root_to_canonical_configinfo(
    monkeypatch: pytest.MonkeyPatch,
    module_suffix: str,
) -> None:
    """A proven root map preserves the resolved BSL module ordinal."""
    source_root = "a" * 40
    source_descriptor = "c" * 40
    source_module = "b" * 40
    cache_calls = 0
    refreshed: list[dict[str, Any]] = []
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda _base_id: ({"database": "upo_test"}, None))
    def fake_cache_lookup(*_args: Any, **_kwargs: Any) -> list[dict[str, Any]]:
        nonlocal cache_calls
        cache_calls += 1
        return [] if cache_calls == 1 else [{"id": 1, "extension_name": "test-extension", "name": "TestReport"}]

    monkeypatch.setattr(adapter_server, "extension_route_cache_lookup", fake_cache_lookup)
    monkeypatch.setattr(
        adapter_server,
        "extension_objects_find",
        lambda payload: refreshed.append(payload) or {"status": "ok", "objects": []},
    )
    monkeypatch.setattr(
        adapter_server,
        "extension_route_cache_row_to_match",
        lambda *_args, **_kwargs: {
            "kind": "Report",
            "guid": "e064659b-6bf8-4162-8c3d-ae186c151ff1",
            "name": "TestReport",
            "origin": {"extension": {"name": "test-extension", "guid": "8e02accd-8a34-11f1-8294-005056b0d483"}},
            "route": {"file_name": source_root, "manifest_entry": {"root_cas_key": source_root}},
            "manifest_entries": [
                {"cas_key": source_descriptor, "suffix": "", "root_cas_key": source_root},
                {"cas_key": source_module, "suffix": module_suffix, "root_cas_key": source_root},
            ],
        },
    )
    monkeypatch.setattr(adapter_server, "metadata_object_forms", lambda *_args, **_kwargs: {"forms": []})

    file_names, object_card, error = adapter_server.saved_state_prepare_file_names(
        {
            "base_id": "upo_test",
            "extension": "test-extension",
            "ref": "Report.TestReport",
        },
        "upo_test",
        "ConfigCAS",
        30,
    )

    assert error is None
    assert file_names == sorted([source_root, source_descriptor, source_module])
    assert object_card is not None
    assert object_card["saved_state_protocol"] == "canonical_configinfo_pair"
    assert object_card["saved_state_name_map"] == {
        source_root: "8e02accd-8a34-11f1-8294-005056b0d483__configinfo",
        source_descriptor: "8e02accd-8a34-11f1-8294-005056b0d483__e064659b-6bf8-4162-8c3d-ae186c151ff1",
        source_module: f"8e02accd-8a34-11f1-8294-005056b0d483__e064659b-6bf8-4162-8c3d-ae186c151ff1{module_suffix}",
    }
    assert len(refreshed) == 1
    assert refreshed[0]["refresh_cache"] is False


def test_extension_prepare_blocks_hash_key_copy_without_proven_working_copy_codec(monkeypatch: pytest.MonkeyPatch) -> None:
    """SQL-readable hash copies must not be advertised as Configurator-ready."""
    monkeypatch.setattr(adapter_server, "repository_apply_gate", lambda *_args: None)
    monkeypatch.setattr(
        adapter_server,
        "saved_state_prepare_file_names",
        lambda *_args: (
            ["a" * 40, "b" * 40],
            {"saved_state_protocol": "source_key_copy", "saved_state_name_map": {}},
            None,
        ),
    )

    result = adapter_server.metadata_saved_state_prepare(
        {
            "base_id": "upo_test",
            "extension": "test-extension",
            "layer": "extension_saved_state",
            "ref": "Report.TestReport",
            "mode": "plan",
            "include_storage": True,
        }
    )

    assert result["status"] == "extension_saved_state_prepare_protocol_unproven"
    assert result["applied"] is False
    assert result["saved_state_protocol"] == "source_key_copy"


def test_saved_state_prepare_verify_merge_preserves_existing_extension_configinfo(monkeypatch: pytest.MonkeyPatch) -> None:
    """A pending ConfigInfo map is preserved, not compared to active ConfigCAS."""
    source_rows = [
        {"FileName": "active-root", "PartNo": 0, "DataSize": 10, "BinaryBytes": 10, "BinarySHA1": "A" * 40},
        {"FileName": "active-descriptor", "PartNo": 0, "DataSize": 11, "BinaryBytes": 11, "BinarySHA1": "B" * 40},
        {"FileName": "active-module", "PartNo": 0, "DataSize": 12, "BinaryBytes": 12, "BinarySHA1": "C" * 40},
    ]
    existing_configinfo = {"FileName": "ext__configinfo", "PartNo": 0, "DataSize": 13, "BinaryBytes": 13, "BinarySHA1": "D" * 40}
    copied_rows = [
        {"FileName": "ext__object", "PartNo": 0, "DataSize": 11, "BinaryBytes": 11, "BinarySHA1": "B" * 40},
        {"FileName": "ext__object.0", "PartNo": 0, "DataSize": 12, "BinaryBytes": 12, "BinarySHA1": "C" * 40},
    ]
    target_reads = 0

    monkeypatch.setattr(adapter_server, "repository_apply_gate", lambda *_args: None)
    monkeypatch.setattr(
        adapter_server,
        "saved_state_prepare_file_names",
        lambda *_args: (
            ["active-root", "active-descriptor", "active-module"],
            {
                "saved_state_name_map": {
                    "active-root": "ext__configinfo",
                    "active-descriptor": "ext__object",
                    "active-module": "ext__object.0",
                },
            },
            None,
        ),
    )

    def fake_rows(_base_id: str, table: str, _file_names: list[str], *, timeout_seconds: int = 30):
        nonlocal target_reads
        if table == "ConfigCAS":
            return source_rows, {"database": "upo_test"}, None
        target_reads += 1
        rows = [existing_configinfo] if target_reads == 1 else [existing_configinfo, *copied_rows]
        return rows, {"database": "upo_test"}, None

    monkeypatch.setattr(adapter_server, "saved_state_copy_row_details", fake_rows)
    monkeypatch.setattr(
        adapter_server,
        "apply_saved_state_prepare_copy",
        lambda *_args, **_kwargs: {"status": "applied", "applied": True, "counts": {"inserted_rows": 2}},
    )

    result = adapter_server.metadata_saved_state_prepare(
        {
            "base_id": "upo_test",
            "extension": "test2",
            "layer": "extension_saved_state",
            "ref": "CommonForm.t_Форма",
            "mode": "apply_and_verify",
            "allow_sql_saved_state_prepare": True,
            "include_storage": True,
        }
    )

    assert result["status"] == "verified"
    assert result["verification"] == {"status": "ok", "expected_rows": 3, "actual_rows": 3, "mismatched": []}


def test_saved_state_prepare_default_is_name_first_and_hides_storage(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, str] = {}

    def fake_file_names(payload: dict[str, Any], base_id: str, source_table: str, timeout_seconds: int):
        seen["source_table"] = source_table
        seen["kind"] = str(payload.get("kind") or "")
        seen["name"] = str(payload.get("name") or "")
        return ["owner__module.0"], {"guid": "owner", "kind": "CommonForm", "name": "t_Форма"}, None

    def fake_row_details(base_id: str, table: str, file_names: list[str], *, timeout_seconds: int = 30):
        seen.setdefault("target_table", table)
        rows = (
            [{"FileName": "owner__module.0", "PartNo": 0, "DataSize": 10, "BinaryBytes": 10, "BinarySHA1": "A" * 40}]
            if table == "ConfigCAS"
            else []
        )
        return rows, {"database": "upo_test"}, None

    monkeypatch.setattr(adapter_server, "saved_state_prepare_file_names", fake_file_names)
    monkeypatch.setattr(adapter_server, "saved_state_copy_row_details", fake_row_details)

    result = adapter_server.metadata_saved_state_prepare(
        {
            "base_id": "upo_test",
            "layer": "extension_saved_state",
            "ref": "CommonForm.t_Форма",
        }
    )

    assert seen == {
        "source_table": "ConfigCAS",
        "kind": "CommonForm",
        "name": "t_Форма",
        "target_table": "ConfigCAS",
    }
    assert result["status"] == "plan_ready"
    assert result["source"] == {
        "kind": "active_configuration",
        "layer": "extension_configuration",
    }
    assert result["target"] == {"layer": "extension_saved_state"}
    assert result["object"] == {
        "kind": "CommonForm",
        "name": "t_Форма",
        "ref": "CommonForm.t_Форма",
    }
    assert result["counts"] == {
        "selected_objects": 1,
        "source_records": 1,
        "existing_saved_records": 0,
        "inserted_records": 0,
    }
    serialized = json.dumps(result, ensure_ascii=False)
    for storage_marker in ("ConfigCASSave", "ConfigCAS", "owner__module.0", "BinarySHA1", "database", "file_names", "source_rows"):
        assert storage_marker not in serialized


def test_saved_state_ensure_plans_missing_copy_without_configurator(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_prepare(payload: dict[str, Any]) -> dict[str, Any]:
        seen.update(payload)
        return {"status": "plan_ready", "base_id": "upo_test", "applied": False}

    monkeypatch.setattr(adapter_server, "metadata_saved_state_prepare", fake_prepare)

    result = adapter_server.metadata_saved_state_ensure(
        {"base_id": "upo_test", "extension": "test2", "ref": "Report.Тест"}
    )

    assert seen["merge_missing"] is True
    assert result["status"] == "plan_ready"
    assert result["saved_state"] == {
        "ready": False,
        "created": False,
        "next_action": "apply_with_allow_sql_saved_state_prepare",
        "manual_configurator_required": False,
        "manual_configurator_note": "A missing saved-state copy is created by the adapter; do not ask the user to create it in Configurator.",
    }


def test_saved_state_ensure_is_ready_when_copy_already_exists(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_prepare",
        lambda _payload: {"status": "already_prepared", "base_id": "upo_test", "applied": False},
    )

    result = adapter_server.metadata_saved_state_ensure({"base_id": "upo_test", "ref": "Catalog.Номенклатура"})

    assert result["status"] == "ready"
    assert result["saved_state"]["ready"] is True
    assert result["saved_state"]["next_action"] == "write_saved_state"


def test_saved_state_prepare_exposes_opaque_rollback_receipt_without_storage_details() -> None:
    receipt_id = "a" * 32

    result = adapter_server.public_saved_state_prepare_result(
        {
            "status": "verified",
            "applied": True,
            "apply_result": {
                "counts": {"inserted_rows": 1},
                "prepare_receipt": {
                    "receipt_id": receipt_id,
                    "path": "C:/private/prepare-receipts/receipt.json",
                    "rows": 1,
                },
            },
        },
        base_id="upo_test",
        target_table="ConfigSave",
        object_card={"kind": "Catalog", "name": "Номенклатура"},
    )

    assert result["prepare_receipt_id"] == receipt_id
    assert "prepare-receipts" not in json.dumps(result, ensure_ascii=False)


def test_code_write_accepts_exact_public_ref_for_apply_without_opaque_write_target() -> None:
    result = adapter_server.code_write_strict_target_error(
        {
            "ref": "Report.ТестовыйОтчет",
            "old": "СтарыйФрагмент",
            "new": "НовыйФрагмент",
            "occurrences": 1,
        },
        require_explicit_target=True,
    )

    assert result is None


def test_code_write_apply_routes_exact_public_ref_without_opaque_write_target(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_metadata_write(payload: dict[str, Any]) -> dict[str, Any]:
        seen["payload"] = payload
        return {
            "schema": "onec_metadata_write.v1",
            "method": "metadata.write",
            "status": "verified",
            "execution_mode": "apply_and_verify",
            "target_kind": "module",
            "base_id": "upo_test",
            "routed_method": "metadata.module.write_apply",
            "result": {
                "status": "verified",
                "applied": True,
                "verification": {"status": "ok", "new_occurrences": 1},
                "apply_result": {"backup": {"backup_id": "b" * 32}},
            },
        }

    monkeypatch.setattr(adapter_server, "metadata_write", fake_metadata_write)

    result = adapter_server.code_write(
        {
            "base_id": "upo_test",
            "ref": "Catalog.Контрагенты",
            "old": "СтарыйФрагмент",
            "new": "НовыйФрагмент",
            "execution_mode": "apply_and_verify",
            "allow_sql_saved_state_apply": True,
        }
    )

    assert result["status"] == "verified"
    assert result["applied"] is True
    assert seen["payload"]["target"] == {
        "kind": "module",
        "object_type": "Catalog",
        "object_name": "Контрагенты",
    }
    assert result["verification"] == {"status": "ok", "new_occurrences": 1}
    assert result["rollback"]["method"] == "metadata.write.rollback"
    assert result["rollback"]["params"]["backup_id"] == "b" * 32


def test_code_write_public_ref_apply_auto_prepares_missing_saved_state(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_metadata_write(payload: dict[str, Any]) -> dict[str, Any]:
        seen["payload"] = payload
        return {"status": "needs_prepare", "result": {"status": "needs_prepare", "applied": False}}

    monkeypatch.setattr(adapter_server, "metadata_write", fake_metadata_write)

    adapter_server.code_write(
        {
            "base_id": "upo_test",
            "ref": "Catalog.Контрагенты",
            "old": "СтарыйФрагмент",
            "new": "НовыйФрагмент",
            "execution_mode": "apply_and_verify",
            "allow_sql_saved_state_apply": True,
        }
    )

    assert seen["payload"]["auto_prepare_saved_state"] is True
    assert seen["payload"]["allow_sql_saved_state_prepare"] is True


def test_code_write_public_retry_already_applied_does_not_prepare_or_write(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[dict[str, Any]] = []

    monkeypatch.setattr(
        adapter_server,
        "code_read",
        lambda _payload: {
            "status": "ok",
            "text": "Процедура Печать()\nНоваяСтрока.Выбран = ЗначениеЗаполнено(Количество);\nКонецПроцедуры",
        },
    )
    monkeypatch.setattr(adapter_server, "metadata_write", lambda payload: calls.append(payload) or {"status": "error"})

    result = adapter_server.code_write(
        {
            "base_id": "upo_test",
            "ref": "Report.ПечатьЦенников",
            "extension": "фс_Отчеты",
            "routine_name": "Печать",
            "old": "НоваяСтрока.Выбран = Истина;",
            "new": "НоваяСтрока.Выбран = ЗначениеЗаполнено(Количество);",
            "execution_mode": "apply_and_verify",
            "allow_sql_saved_state_apply": True,
        }
    )

    assert result["status"] == "already_applied"
    assert result["applied"] is False
    assert result["counts"] == {"occurrences": 0, "new_occurrences": 1}
    assert calls == []


def test_code_write_apply_and_rollback_cleans_auto_prepared_rows(monkeypatch: pytest.MonkeyPatch) -> None:
    receipt_id = "a" * 32
    seen: dict[str, Any] = {}

    monkeypatch.setattr(
        adapter_server,
        "metadata_write",
        lambda _payload: {
            "status": "verified_and_rolled_back",
            "base_id": "upo_test",
            "routed_method": "metadata.module.write_apply",
            "prepare_receipt_id": receipt_id,
            "result": {"applied": True, "apply_result": {"applied": True, "readback": {"verified": True}}},
        },
    )

    def fake_cleanup(base_id: str, received_receipt_id: str, *, timeout_seconds: int) -> dict[str, Any]:
        seen.update({"base_id": base_id, "receipt_id": received_receipt_id, "timeout_seconds": timeout_seconds})
        return {"status": "rolled_back", "applied": True, "counts": {"deleted_rows": 4}}

    monkeypatch.setattr(adapter_server, "rollback_saved_state_prepare_receipt", fake_cleanup)

    result = adapter_server.code_write(
        {
            "base_id": "upo_test",
            "ref": "Report.АвтоматическиеСкидки",
            "module_ordinal": 1,
            "old": "старый",
            "new": "новый",
            "mode": "apply_and_rollback",
            "timeout_seconds": 45,
        }
    )

    assert result["status"] == "verified_and_rolled_back"
    assert result["prepare_receipt_id"] == receipt_id
    assert result["prepare_cleanup"] == {"status": "rolled_back", "applied": True, "counts": {"deleted_rows": 4}}
    assert seen == {"base_id": "upo_test", "receipt_id": receipt_id, "timeout_seconds": 45}


def test_code_write_hides_internal_saved_state_route_hints(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_write",
        lambda _payload: {
            "status": "not_found",
            "result": {
                "error": "module_target_not_resolved",
                "diagnostics": {"message": "Pass module_ref or stream_index."},
                "next_resolution": {"method": "metadata.saved_state.prepare", "params": {"file_name": "internal.0"}},
            },
        },
    )

    result = adapter_server.code_write(
        {
            "base_id": "upo_test",
            "ref": "Report.Тест",
            "module_ordinal": 1,
            "old": "старый",
            "new": "новый",
            "mode": "apply_and_verify",
        }
    )

    assert result["error"] == "public_write_route_unresolved"
    assert result["diagnostics"]["next_action"] == "adapter_development_required"
    assert result["route_resolution"] == {
        "error": "module_target_not_resolved",
    }
    assert "next_call" not in result
    assert "module_ref" not in str(result)
    assert "stream_index" not in str(result)


def test_adapter_audit_result_summary_keeps_public_route_diagnostics_only() -> None:
    summary = adapter_server.adapter_audit_result_summary(
        {
            "status": "not_found",
            "error": "public_write_route_unresolved",
            "timings_ms": {"metadata_write": 152000},
            "route_resolution": {
                "method": "metadata.object.modules",
                "status": "not_found",
                "error": "module_target_not_resolved",
                "candidates": 0,
            },
            "diagnostics": {"next_action": "adapter_development_required", "message": "private text"},
            "module_ref": "ConfigCASSave:private.0#stream:1",
        }
    )

    assert summary["route_resolution"]["error"] == "module_target_not_resolved"
    assert summary["diagnostics"] == {"next_action": "adapter_development_required"}
    assert "module_ref" not in str(summary)
    assert "private text" not in str(summary)


def test_public_module_role_does_not_invent_command_from_suffix() -> None:
    role = adapter_server.public_module_role(owner_kind="Report", suffix="2", ordinal=2)

    assert role["kind"] == "bsl_module"
    assert role["role_status"] == "unconfirmed"
    assert role["configurator_path_status"] == "unconfirmed"


def test_code_search_active_hit_pins_runtime_view_in_read_selector(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "search_modules",
        lambda _payload: {
            "status": "ok",
            "source": {"kind": "live_metadata"},
            "matches": [
                {
                    "snippet": {"text": "Фрагмент", "offset": 0},
                    "activation_state": "active",
                    "owner": {"kind": "Report", "name": "Тест"},
                    "module": {"kind": "bsl_module", "module_ordinal": 2},
                    "origin": {"source": "extension"},
                    "read_selector": {"base_id": "upo_test", "kind": "Report", "name": "Тест", "module_ordinal": 2},
                }
            ],
            "counts": {"matches": 1},
        },
    )

    result = adapter_server.code_search({"base_id": "upo_test", "ref": "Report.Тест", "query": "Фрагмент"})

    assert result["items"][0]["read_selector"]["configuration_view"] == "runtime_applied"


def test_modules_search_enforces_total_timeout_before_extension_owner_scan(monkeypatch: pytest.MonkeyPatch) -> None:
    clock = iter((0.0, 0.0, 2.0, 2.0))
    monkeypatch.setattr(adapter_server.time, "monotonic", lambda: next(clock))
    monkeypatch.setattr(
        adapter_server,
        "extension_filter_to_guid",
        lambda *_args, **_kwargs: ("11111111-1111-1111-1111-111111111111", None),
    )
    monkeypatch.setattr(adapter_server, "extension_definition_guid_sources", lambda *_args, **_kwargs: ({}, None))
    monkeypatch.setattr(
        adapter_server,
        "extension_objects_find",
        lambda *_args, **_kwargs: pytest.fail("owner scan must not start after the total deadline"),
    )

    result = adapter_server.search_modules(
        {"base_id": "upo_test", "extension": "ТестовоеРасширение", "query": "Процедура", "timeout_seconds": 1}
    )

    assert result["status"] == "partial"
    assert result["counts"]["time_budget_exhausted"] is True
    assert result["diagnostics"]["stage"] == "extension_objects_find"


def test_help_methods_registry_does_not_depend_on_object_decode_locals() -> None:
    result = adapter_server.call_method("help.methods", {})

    assert result["schema"] == "onec_adapter_methods.v1"
    assert any(item.get("name") == "code.write" for item in result["methods"])


def test_saved_state_object_module_search_resolves_hash_keyed_extension_stream(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_modules",
        lambda _payload: {
            "status": "ok",
            "object": {"guid": "a" * 36, "kind": "Report", "name": "Тест", "synonym": "Тест"},
            "modules": [
                {
                    "module_id": "ConfigCAS:hash-key#stream:4",
                    "kind": "object_module",
                    "qualified_name": "Тест.Модуль объекта",
                }
            ],
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "read_module",
        lambda _payload: {"status": "ok", "text": "Процедура Проверка()\nКонецПроцедуры", "content_sha1": "b" * 40},
    )

    rows = adapter_server.saved_state_object_module_rows_from_public_owner(
        base_id="upo_test",
        ref="Report.Тест",
        extension="ТестРасширение",
        query="Проверка",
        timeout_seconds=30,
    )

    assert len(rows) == 1
    assert rows[0]["owner"]["name"] == "Тест"
    assert rows[0]["streams"][0]["module_ref"] == "ConfigCASSave:hash-key#stream:4"
    assert rows[0]["streams"][0]["match"]["in_text"] is True
    assert rows[0]["streams"][0]["expected_text_sha1"] == "b" * 40


def test_public_extension_object_module_resolver_maps_active_hash_to_canonical_saved_name(monkeypatch: pytest.MonkeyPatch) -> None:
    """High-level writes must not ask a caller for ConfigCASSave coordinates."""
    active_hash = "a" * 40
    canonical_saved_name = "8e02accd-8a34-11f1-8294-005056b0d483__e064659b-6bf8-4162-8c3d-ae186c151ff1.0"
    seen: dict[str, Any] = {}
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_modules",
        lambda _payload: {
            "status": "ok",
            "modules": [
                {"kind": "object_module", "module_ordinal": 2, "module_id": f"ConfigCAS:{'c' * 40}#stream:4"},
                {"kind": "object_module", "module_ordinal": 1, "module_id": f"ConfigCAS:{active_hash}#stream:4"},
            ],
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_prepare",
        lambda _payload: {
            "status": "already_prepared",
            "source_rows": [{"source_file_name": active_hash, "file_name": canonical_saved_name}],
        },
    )

    def fake_read_module(payload: dict[str, Any]) -> dict[str, Any]:
        seen["module_ref"] = payload["module_ref"]
        return {
            "status": "ok",
            "text": "Процедура Проверка()\nКонецПроцедуры",
            "content_sha1": "b" * 40,
            "write_preconditions": {"expected_text_sha1": "b" * 40},
        }

    monkeypatch.setattr(adapter_server, "read_module", fake_read_module)
    target, resolution = adapter_server.metadata_write_resolve_public_object_module_saved_ref(
        {
            "base_id": "upo_test",
            "extension": "фс_Отчеты1",
            "ref": "Report.фс_ПродовольственнаяКорзина",
            "module_ordinal": 1,
            "old": "Проверка",
        }
    )

    assert target is not None
    assert target["module_ref"] == f"ConfigCASSave:{canonical_saved_name}#stream:4"
    assert seen["module_ref"] == target["module_ref"]
    assert resolution == {"method": "metadata.object.modules", "status": "resolved_public_object_module"}


def test_public_extension_object_module_resolver_does_not_use_old_fragment_as_route_key(monkeypatch: pytest.MonkeyPatch) -> None:
    """A prior successful write removes old, but not the confirmed owner route."""
    active_hash = "a" * 40
    saved_name = "8e02accd-8a34-11f1-8294-005056b0d483__e064659b-6bf8-4162-8c3d-ae186c151ff1.2"
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_modules",
        lambda _payload: {"status": "ok", "modules": [{"kind": "object_module", "module_ordinal": 2, "module_id": f"ConfigCAS:{active_hash}#stream:4"}]},
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_prepare",
        lambda _payload: {"status": "already_prepared", "source_rows": [{"source_file_name": active_hash, "file_name": saved_name}]},
    )
    monkeypatch.setattr(
        adapter_server,
        "read_module",
        lambda _payload: {"status": "ok", "text": "// replacement already applied", "content_sha1": "b" * 40},
    )

    target, resolution = adapter_server.metadata_write_resolve_public_object_module_saved_ref(
        {"base_id": "upo_test", "extension": "фс_Отчеты", "ref": "Report.Тест", "module_ordinal": 2, "old": "gone"}
    )

    assert target is not None
    assert target["module_ref"] == f"ConfigCASSave:{saved_name}#stream:4"
    assert resolution["status"] == "resolved_public_object_module"


def test_module_scope_fragment_reports_already_applied_for_exact_idempotent_retry(monkeypatch: pytest.MonkeyPatch) -> None:
    routine = "Процедура Тест()\nНовыйФрагмент;\nКонецПроцедуры"
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *_args, **_kwargs: (b"unused", {}, None))
    monkeypatch.setattr(
        "parser.cas_payload.classify_payload",
        lambda *_args, **_kwargs: {"stream_blocks": [{"text": routine}]},
    )

    result = adapter_server.metadata_module_write_scope_fragment_payload(
        {"_force_fragment_replace": True, "routine_name": "Тест", "old": "СтарыйФрагмент;", "new": "НовыйФрагмент;"},
        base_id="upo_test", table="ConfigCASSave", file_name="module.2", stream_index=0, timeout_seconds=30,
        method="metadata.module.write_apply",
    )

    assert result["status"] == "already_applied"
    assert result["counts"] == {"occurrences": 0, "new_occurrences": 1}


def test_saved_state_modules_search_uses_object_name_for_extension_object_module(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "extension_filter_to_guid", lambda *_args, **_kwargs: ("a" * 36, None))
    monkeypatch.setattr(adapter_server, "metadata_saved_state_modules_owner_guid_from_selector", lambda *_args, **_kwargs: (None, {"status": "resolved"}))
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_modules",
        lambda payload: {
            "status": "ok",
            "object": {"guid": "b" * 36, "kind": "Report", "name": "Тест"},
            "modules": [{"module_id": "ConfigCAS:hash-key#stream:2", "kind": "object_module", "qualified_name": "Тест.Модуль объекта"}],
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "read_module",
        lambda _payload: {"status": "ok", "text": "Процедура Проверка()\nКонецПроцедуры", "content_sha1": "c" * 40},
    )
    monkeypatch.setattr(adapter_server, "storage_files_list", lambda _payload: (_ for _ in ()).throw(AssertionError("raw scan must not run")))

    result = adapter_server.metadata_saved_state_modules_search(
        {
            "base_id": "upo_test",
            "extension": "ТестРасширение",
            "object_type": "Report",
            "object_name": "Тест",
            "module": "object_module",
            "query": "Проверка",
            "include_storage": True,
        }
    )

    assert result["status"] == "ok"
    stream = result["modules"][0]["streams"][0]
    assert result["modules"][0]["owner"]["name"] == "Тест"
    assert result["modules"][0]["module"]["kind"] == "object_module"
    assert stream["module_ref"] == "ConfigCASSave:hash-key#stream:2"
    assert stream["expected_text_sha1"] == "c" * 40


def test_code_write_reclassifies_public_module_text_sha_as_text_precondition(monkeypatch: pytest.MonkeyPatch) -> None:
    text_sha1 = "c" * 40
    monkeypatch.setattr(
        adapter_server,
        "metadata_write_resolve_public_object_module_saved_ref",
        lambda _payload: (
            {
                "module_ref": "ConfigCASSave:hash-key#stream:4",
                "expected_text_sha1": text_sha1,
            },
            {"method": "metadata.object.modules", "status": "resolved_public_object_module"},
        ),
    )

    resolved = adapter_server.metadata_write_resolve_module_target(
        {
            "base_id": "upo",
            "ref": "Report.Тест",
            "old": "СтарыйФрагмент",
            "new": "НовыйФрагмент",
            "expected_sha1": text_sha1,
        },
        {},
        "plan",
    )

    assert isinstance(resolved, tuple)
    write_payload, _resolution = resolved
    assert write_payload["module_ref"] == "ConfigCASSave:hash-key#stream:4"
    assert write_payload["expected_text_sha1"] == text_sha1
    assert "expected_sha1" not in write_payload


def test_module_write_plan_hint_preserves_text_sha_precondition() -> None:
    text_sha1 = "d" * 40
    hint = adapter_server.metadata_write_plan_apply_payload_hint(
        {"base_id": "upo_test", "expected_text_sha1": text_sha1, "old": "old", "new": "new"},
        {"kind": "module", "module_ref": "ConfigSave:test.0#stream:4"},
        {},
        path_resolution={},
        target_kind="module",
        operation_class="replace",
        concrete_reference="ConfigSave:test.0#stream:4",
    )

    assert hint is not None
    assert hint["payload"]["expected_text_sha1"] == text_sha1


def test_code_write_plan_returns_name_first_apply_hint(monkeypatch: pytest.MonkeyPatch) -> None:
    text_sha1 = "e" * 40
    monkeypatch.setattr(
        adapter_server,
        "metadata_write",
        lambda _payload: {
            "status": "planned",
            "routed_method": "metadata.module.write_apply",
            "result": {"status": "planned", "applied": False},
        },
    )

    result = adapter_server.code_write(
        {
            "base_id": "upo",
            "ref": "Report.Тест",
            "extension": "ТестРасширение",
            "old": "СтарыйФрагмент",
            "new": "НовыйФрагмент",
            "execution_mode": "plan",
            "repository_lock": {"lock_session_id": "lock-1"},
        }
    )

    # The resolver normally adds this guard; isolate the response contract in
    # this test by setting it on the payload seen by code_write.
    assert result["apply_hint"]["method"] == "code.write"
    assert result["apply_hint"]["params"]["ref"] == "Report.Тест"
    assert result["apply_hint"]["params"]["extension"] == "ТестРасширение"
    assert "module_ref" not in result["apply_hint"]["params"]


def test_code_write_plan_returns_bounded_fragment_diff(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_write",
        lambda _payload: {
            "status": "planned",
            "routed_method": "metadata.module.write_apply",
            "result": {
                "status": "planned",
                "proposal": {
                    "counts": {"edits": 1},
                    "edits": [{"old_text_sha1": "a" * 40, "new_text_sha1": "b" * 40}],
                },
            },
        },
    )

    result = adapter_server.code_write(
        {"base_id": "upo", "ref": "Report.Тест", "old": "old", "new": "new", "execution_mode": "plan"}
    )

    assert result["fragment_replace"]["status"] == "planned_exact_replace"
    assert result["fragment_replace"]["planned_edits"] == 1
    assert result["fragment_replace"]["diff"] == {"removed": "old", "added": "new"}


def test_code_write_plan_uses_contextual_anchors_without_replacing_them(monkeypatch: pytest.MonkeyPatch) -> None:
    captured: dict[str, Any] = {}

    def fake_metadata_write(payload: dict[str, Any]) -> dict[str, Any]:
        captured.update(payload)
        return {
            "status": "planned",
            "routed_method": "metadata.module.write_apply",
            "result": {"status": "planned", "proposal": {"counts": {"edits": 1}}},
        }

    monkeypatch.setattr(adapter_server, "metadata_write", fake_metadata_write)

    result = adapter_server.code_write(
        {
            "base_id": "upo",
            "ref": "Report.Тест",
            "before": "Начало\n",
            "old": "ПовторяемыйФрагмент",
            "after": "\nКонец",
            "new": "ИсправленныйФрагмент",
            "execution_mode": "plan",
        }
    )

    # Uniqueness is checked on the whole contextual fragment; the replacement
    # itself preserves both anchors byte-for-byte.
    assert captured["old"] == "Начало\nПовторяемыйФрагмент\nКонец"
    assert captured["new"] == "Начало\nИсправленныйФрагмент\nКонец"
    assert result["fragment_replace"]["diff"] == {
        "removed": "ПовторяемыйФрагмент",
        "added": "ИсправленныйФрагмент",
    }
    assert result["fragment_replace"]["context"] == {"before": "Начало\n", "after": "\nКонец"}
    hint = result["apply_hint"]["params"]
    assert {key: hint[key] for key in ("base_id", "ref", "old", "new", "execution_mode", "before", "after")} == {
        "base_id": "upo",
        "ref": "Report.Тест",
        "old": "ПовторяемыйФрагмент",
        "new": "ИсправленныйФрагмент",
        "execution_mode": "apply_and_verify",
        "before": "Начало\n",
        "after": "\nКонец",
    }


def test_code_replacement_candidates_returns_safe_context_not_ordinals() -> None:
    source = "До\nПовтор\nПослеПервого\nДо\nПовтор\nПослеВторого"

    candidates = adapter_server.code_replacement_candidates(source, "Повтор", anchor_chars=20)

    assert len(candidates) == 2
    assert [candidate["line"] for candidate in candidates] == [2, 5]
    assert all("ordinal" not in candidate for candidate in candidates)
    assert all(candidate["status"] == "ready" for candidate in candidates)
    assert candidates[0]["before"] != candidates[1]["before"] or candidates[0]["after"] != candidates[1]["after"]


def test_code_write_accepts_ready_replacement_candidate(monkeypatch: pytest.MonkeyPatch) -> None:
    captured: dict[str, Any] = {}
    monkeypatch.setattr(
        adapter_server,
        "metadata_write",
        lambda payload: (captured.update(payload) or {"status": "planned", "result": {"status": "planned"}}),
    )

    result = adapter_server.code_write(
        {
            "replacement_candidate": {
                "status": "ready",
                "before": "До\n",
                "old": "Старое",
                "after": "\nПосле",
                "write_selector": {
                    "base_id": "upo_test",
                    "ref": "Report.Тест",
                    "extension": "ТестРасширение",
                    "expected_text_sha1": "a" * 40,
                },
            },
            "new": "Новое",
            "execution_mode": "plan",
        }
    )

    assert captured["old"] == "До\nСтарое\nПосле"
    assert captured["new"] == "До\nНовое\nПосле"
    assert captured["expected_text_sha1"] == "a" * 40
    assert result["apply_hint"]["params"]["ref"] == "Report.Тест"


def test_saved_state_ensure_is_exposed_through_public_dispatch(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_prepare",
        lambda _payload: {"status": "already_prepared", "base_id": "upo_test", "applied": False},
    )

    result = call_method("metadata.saved_state.ensure", {"base_id": "upo_test", "ref": "Catalog.Номенклатура"})

    assert result["schema"] == "onec_saved_state_ensure.v1"
    assert result["status"] == "ready"


def test_saved_state_ensure_can_be_enqueued_as_job(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_start_job(payload: dict[str, Any]) -> dict[str, Any]:
        seen.update(payload)
        return {"schema": "onec_adapter_job.v1", "status": "accepted", "job_id": "job-1"}

    monkeypatch.setattr(adapter_server, "adapter_start_job", fake_start_job)

    result = call_method(
        "metadata.saved_state.ensure",
        {"base_id": "upo_test", "ref": "Catalog.Номенклатура", "execution_mode": "job"},
    )

    assert result["status"] == "accepted"
    assert seen["method"] == "metadata.saved_state.ensure"
    assert seen["payload"] == {"base_id": "upo_test", "ref": "Catalog.Номенклатура"}


def test_saved_state_ensure_rollback_requires_receipt_and_delegates(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_rollback(base_id: str, receipt_id: str, *, timeout_seconds: int) -> dict[str, Any]:
        seen.update(base_id=base_id, receipt_id=receipt_id, timeout_seconds=timeout_seconds)
        return {"status": "applied", "applied": True}

    monkeypatch.setattr(adapter_server, "rollback_saved_state_prepare_receipt", fake_rollback)
    receipt_id = "a" * 32

    result = call_method(
        "metadata.saved_state.ensure.rollback",
        {"base_id": "upo_test", "prepare_receipt_id": receipt_id, "allow_sql_saved_state_rollback": True},
    )

    assert result["schema"] == "onec_saved_state_ensure_rollback.v1"
    assert result["status"] == "applied"
    assert seen == {"base_id": "upo_test", "receipt_id": receipt_id, "timeout_seconds": 60}


def test_saved_state_prepare_rejects_conflicting_semantic_and_storage_layers() -> None:
    result = adapter_server.metadata_saved_state_prepare(
        {
            "base_id": "upo_test",
            "layer": "base_saved_state",
            "target_table": "ConfigCASSave",
            "ref": "Catalog.Номенклатура",
        }
    )

    assert result["status"] == "invalid_argument"
    assert result["argument"] == "layer"


def test_saved_state_prepare_resolves_extension_route_cache(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"database": base_id, "server": "s"}, None))
    monkeypatch.setattr(
        adapter_server,
        "extension_route_cache_lookup",
        lambda config, query, kind_filter, guid_filter, extension_guid, limit: [
            {
                "extension_name": "test2",
                "extension_guid": "ext-guid",
                "object_kind": "CommonForm",
                "name": "t_Форма",
                "guid": "form-guid",
                "descriptor_cas_key": "descriptor",
                "route_json": json.dumps({"table": "ConfigCAS", "file_name": "descriptor"}, ensure_ascii=False),
                "manifest_entries_json": json.dumps(
                    [
                        {"suffix": "", "cas_key": "descriptor"},
                        {"suffix": ".0", "cas_key": "payload"},
                    ],
                    ensure_ascii=False,
                ),
            },
            {
                "extension_name": "test2",
                "extension_guid": "ext-guid",
                "object_kind": "CommonForm",
                "name": "tt_Форма3",
                "guid": "other-form-guid",
                "descriptor_cas_key": "other-descriptor",
                "route_json": json.dumps({"table": "ConfigCAS", "file_name": "other-descriptor"}, ensure_ascii=False),
                "manifest_entries_json": json.dumps(
                    [{"suffix": "", "cas_key": "other-descriptor"}],
                    ensure_ascii=False,
                ),
            },
        ],
    )
    monkeypatch.setattr(
        adapter_server,
        "saved_state_copy_row_details",
        lambda base_id, table, file_names, timeout_seconds=30: (
            [{"FileName": name, "PartNo": 0, "DataSize": 10, "BinaryBytes": 10, "BinarySHA1": "A" * 40} for name in file_names] if table == "ConfigCAS" else [],
            {"database": "upo_test"},
            None,
        ),
    )

    result = adapter_server.metadata_saved_state_prepare(
        {
            "base_id": "upo_test",
            "target_table": "ConfigCASSave",
            "extension": "test2",
            "object_type": "CommonForm",
            "object_name": "t_Форма",
            "include_storage": True,
        }
    )

    assert result["status"] == "plan_ready"
    assert result["file_names"] == ["descriptor", "payload"]
    assert result["object"]["mode"] == "extension_route_cache"
    assert result["counts"]["source_rows"] == 2


def test_saved_state_prepare_resolves_extension_form_through_public_owner(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_forms(payload: dict[str, Any]) -> dict[str, Any]:
        seen.update(payload)
        return {
            "status": "ok",
            "forms": [
                {"name": "ФормаОтчета", "source": {"table": "ConfigCAS", "file_name": "ext__form.0"}},
            ],
        }

    monkeypatch.setattr(adapter_server, "metadata_object_forms", fake_forms)
    file_names, object_card, error = adapter_server.saved_state_prepare_file_names(
        {
            "base_id": "upo_test",
            "extension": "фс_Отчеты",
            "ref": "Report.Продкорзина",
            "form": "ФормаОтчета",
        },
        "upo_test",
        "ConfigCAS",
        30,
    )

    assert error is None
    assert file_names == ["ext__form.0"]
    assert object_card["mode"] == "extension_form_owner"
    assert seen["ref"] == "Report.Продкорзина"
    assert seen["form"] == "ФормаОтчета"


def test_saved_state_prepare_rejects_bare_extension_form_name_without_global_scan() -> None:
    file_names, _object_card, error = adapter_server.saved_state_prepare_file_names(
        {"base_id": "upo_test", "extension": "фс_Отчеты", "form": "ФормаОтчета"},
        "upo_test",
        "ConfigCAS",
        30,
    )

    assert file_names == []
    assert error["error"] == "form_owner_required"
    assert error["diagnostics"]["next_call"]["method"] == "metadata.object.forms"


def test_saved_state_prepare_public_form_owner_error_keeps_next_call() -> None:
    result = adapter_server.metadata_saved_state_prepare(
        {"base_id": "upo_test", "extension": "фс_Отчеты", "form": "ФормаОтчета", "mode": "plan"}
    )

    assert result["status"] == "invalid_argument"
    assert result["error"] == "form_owner_required"
    assert result["next_call"]["method"] == "metadata.object.forms"


def test_saved_state_diff_compares_active_and_saved_payload(monkeypatch: pytest.MonkeyPatch) -> None:
    reads: list[tuple[str, str]] = []

    def fake_read_storage_file_bytes(base_id: str, table: str, file_name: str, *, timeout_seconds: int = 30):
        reads.append((table, file_name))
        if table == "ConfigCAS":
            return b'{1,"active"}', {"database": "upo_test"}, None
        if table == "ConfigCASSave":
            return b'{1,"saved"}', {"database": "upo_test"}, None
        raise AssertionError(table)

    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)

    result = adapter_server.metadata_saved_state_diff(
        {
            "base_id": "upo_test",
            "module_ref": "ConfigCASSave:form-module.0#stream:0",
            "max_text_diff_lines": 20,
        }
    )

    assert reads == [("ConfigCASSave", "form-module.0"), ("ConfigCAS", "form-module.0")]
    assert result["schema"] == "onec_saved_state_diff.v1"
    assert result["status"] == "changed"
    assert result["needs_prepare"] is False
    assert result["source"]["active"] == {"table": "ConfigCAS", "file_name": "form-module.0"}
    assert result["source"]["saved"] == {"table": "ConfigCASSave", "file_name": "form-module.0"}
    assert result["current_state"] == {"source": "saved_state", "activation_state": "not_activated"}
    assert result["comparison"]["differs"] is True
    assert result["freshness"]["status"] == "live_sql_verified"


def test_saved_state_diff_uses_selected_bsl_stream_for_text_same(monkeypatch: pytest.MonkeyPatch) -> None:
    def fake_read_storage_file_bytes(base_id: str, table: str, file_name: str, *, timeout_seconds: int = 30):
        value = b"active-container" if table == "ConfigCAS" else b"saved-container"
        return value, {"database": "upo_test"}, None

    def fake_extract(data: bytes, *, module_ref: str, bsl_offset: int | None = None):
        return "Процедура Одинаковая()\nКонецПроцедуры\n", {
            "status": "ok",
            "source": "stream",
            "stream_index": 4,
        }

    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)
    monkeypatch.setattr(adapter_server, "extract_code_index_text_from_payload", fake_extract)

    result = adapter_server.metadata_saved_state_diff(
        {
            "base_id": "upo_test",
            "module_ref": "ConfigCASSave:form-module.0#stream:4",
        }
    )

    assert result["status"] == "changed"
    assert result["comparison"]["differs"] is True
    assert result["comparison"]["container_text_same"] is False
    assert result["comparison"]["module_text_same"] is True
    assert result["comparison"]["text_same"] is True
    assert result["module_text"]["same"] is True
    assert result["module_text"]["active_sha1"] == result["module_text"]["saved_sha1"]


def test_saved_state_diff_falls_back_to_logical_module_when_active_file_name_changed(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    def fake_read_storage_file_bytes(base_id: str, table: str, file_name: str, *, timeout_seconds: int = 30):
        if table == "ConfigCASSave":
            return b"saved-container", {"database": "upo_test"}, None
        return None, {"database": "upo_test"}, {
            "status": "source_missing",
            "error": "source_missing",
            "diagnostics": {"message": "physical CAS key changed"},
        }

    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)
    monkeypatch.setattr(
        adapter_server,
        "saved_state_module_text_comparison_from_live_read",
        lambda *args, **kwargs: {
            "scope": "module",
            "same": False,
            "differs": True,
            "active_sha1": "active-sha1",
            "saved_sha1": "saved-sha1",
            "active_chars": 20,
            "saved_chars": 21,
            "diff_lines": ["-active", "+saved"],
            "diff_truncated": False,
            "counterpart_resolution": {"strategy": "logical_owner_and_module_role"},
        },
    )

    result = adapter_server.metadata_saved_state_diff(
        {
            "base_id": "upo_test",
            "module_ref": "ConfigCASSave:extension__module.0#stream:4",
        }
    )

    assert result["status"] == "changed"
    assert result["comparison"]["module_text_same"] is False
    assert result["comparison"]["container_text_same"] is None
    assert result["module_text"]["counterpart_resolution"]["strategy"] == "logical_owner_and_module_role"
    assert result["freshness"]["status"] == "live_sql_verified"


def test_saved_state_diff_reports_prepare_needed_when_save_missing(monkeypatch: pytest.MonkeyPatch) -> None:
    def fake_read_storage_file_bytes(base_id: str, table: str, file_name: str, *, timeout_seconds: int = 30):
        if table == "ConfigSave":
            return None, {"database": "upo_test"}, {
                "schema": "onec_adapter_source_missing.v1",
                "method": "storage.file.get",
                "status": "source_missing",
                "base_id": base_id,
                "source": {"table": table, "file_name": file_name},
                "diagnostics": {"message": "missing"},
            }
        return b'{1,"active"}', {"database": "upo_test"}, None

    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)

    result = adapter_server.metadata_saved_state_diff(
        {"base_id": "upo_test", "table": "ConfigSave", "file_name": "object.0"}
    )

    assert result["status"] == "not_found"
    assert result["error"] == "saved_state_not_found"
    assert result["needs_prepare"] is True
    assert result["current_state"] == {"source": "active", "activation_state": "active"}
    assert result["prepare_payload"]["method"] == "metadata.saved_state.prepare"
    assert result["prepare_payload"]["target_table"] == "ConfigSave"


def test_saved_state_diff_resolves_named_1c_object_module_without_exposing_storage(monkeypatch: pytest.MonkeyPatch) -> None:
    module_reads: list[dict[str, Any]] = []
    storage_reads: list[tuple[str, str]] = []

    def fake_read_module(payload: dict[str, Any]) -> dict[str, Any]:
        module_reads.append(payload)
        assert payload["kind"] == "Catalog"
        assert payload["name"] == "Номенклатура"
        return {
            "schema": "onec_module_read.v1",
            "status": "ok",
            "module_id": f"{payload['table']}:catalog-guid.2",
            "source": {"kind": "live_sql", "table": payload["table"], "file_name": "catalog-guid.2"},
            "owner": {
                "kind": "Catalog",
                "name": "Номенклатура",
                "synonym": "Номенклатура",
                "guid": "catalog-guid",
            },
            "module": {"kind": "object_module", "name": "Модуль объекта", "module_ordinal": 2},
        }

    def fake_read_storage_file_bytes(base_id: str, table: str, file_name: str, *, timeout_seconds: int = 30):
        storage_reads.append((table, file_name))
        value = b'{1,"saved"}' if table == "ConfigSave" else b'{1,"active"}'
        return value, {"database": "upo_test"}, None

    monkeypatch.setattr(adapter_server, "read_module", fake_read_module)
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)

    result = adapter_server.metadata_saved_state_diff(
        {
            "base_id": "upo_test",
            "object_type": "Catalog",
            "object_name": "Номенклатура",
            "module_ordinal": 2,
        }
    )

    assert [item["table"] for item in module_reads] == ["ConfigSave"]
    assert storage_reads == [("ConfigSave", "catalog-guid.2"), ("Config", "catalog-guid.2")]
    assert result["status"] == "changed"
    assert result["resolved_by"] == "public_object_module_selector"
    assert result["target"]["selector"]["ref"] == "Catalog.Номенклатура"
    assert result["target"]["selector"]["module_ordinal"] == 2
    assert result["target"]["object"]["name"] == "Номенклатура"
    assert "storage" not in result["target"]
    assert result["source"] == {
        "kind": "live_metadata",
        "active": {"state": "active"},
        "saved": {"state": "saved"},
    }


def test_saved_state_diff_named_selector_returns_name_first_prepare_payload(monkeypatch: pytest.MonkeyPatch) -> None:
    def fake_read_module(payload: dict[str, Any]) -> dict[str, Any]:
        if payload["table"] == "ConfigSave":
            return {"status": "source_missing", "error": "source_missing"}
        return {
            "status": "ok",
            "module_id": "Config:catalog-guid.1",
            "source": {"kind": "live_sql", "table": "Config", "file_name": "catalog-guid.1"},
            "owner": {"kind": "Catalog", "name": "Номенклатура", "guid": "catalog-guid"},
            "module": {"kind": "manager_module", "name": "Модуль менеджера", "module_ordinal": 1},
        }

    def fake_read_storage_file_bytes(base_id: str, table: str, file_name: str, *, timeout_seconds: int = 30):
        assert table == "ConfigSave"
        return None, {"database": "upo_test"}, {
            "status": "source_missing",
            "error": "source_missing",
            "diagnostics": {"message": "missing"},
        }

    monkeypatch.setattr(adapter_server, "read_module", fake_read_module)
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)

    result = adapter_server.metadata_saved_state_diff(
        {
            "base_id": "upo_test",
            "ref": "Catalog.Номенклатура",
            "module_ordinal": 1,
        }
    )

    assert result["status"] == "not_found"
    assert result["needs_prepare"] is True
    assert result["prepare_payload"] == {
        "method": "metadata.saved_state.prepare",
        "base_id": "upo_test",
        "layer": "base_saved_state",
        "ref": "Catalog.Номенклатура",
        "kind": "Catalog",
        "name": "Номенклатура",
        "module_ordinal": 1,
        "mode": "plan",
    }
    assert "file_name" not in result["prepare_payload"]


def test_saved_state_diff_named_selector_refreshes_stale_empty_module_cache(monkeypatch: pytest.MonkeyPatch) -> None:
    module_reads: list[tuple[str, bool]] = []

    def fake_read_module(payload: dict[str, Any]) -> dict[str, Any]:
        table = str(payload["table"])
        refresh_cache = bool(payload["refresh_cache"])
        module_reads.append((table, refresh_cache))
        if table == "ConfigSave" or not refresh_cache:
            return {"status": "not_found", "error": "not_found"}
        return {
            "status": "ok",
            "module_id": "Config:common-module-guid.0#stream:4",
            "source": {"kind": "live_sql", "table": "Config", "file_name": "common-module-guid.0"},
            "owner": {"kind": "CommonModule", "name": "ОбщегоНазначения", "guid": "common-module-guid"},
            "module": {
                "kind": "common_module",
                "name": "common-module-guid.0#stream:4",
                "module_ordinal": 1,
            },
        }

    def fake_read_storage_file_bytes(base_id: str, table: str, file_name: str, *, timeout_seconds: int = 30):
        assert table == "ConfigSave"
        return None, {"database": "upo_test"}, {
            "status": "source_missing",
            "error": "source_missing",
        }

    monkeypatch.setattr(adapter_server, "read_module", fake_read_module)
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)

    result = adapter_server.metadata_saved_state_diff(
        {
            "base_id": "upo_test",
            "ref": "CommonModule.ОбщегоНазначения",
            "module_ordinal": 1,
        }
    )

    assert module_reads == [
        ("ConfigSave", False),
        ("ConfigSave", True),
        ("Config", False),
        ("Config", True),
    ]
    assert result["status"] == "not_found"
    assert result["needs_prepare"] is True
    assert result["target"]["module"] == {
        "kind": "common_module",
        "name": "ОбщегоНазначения",
        "module_ordinal": 1,
    }


def test_saved_state_status_classifies_pending_files(monkeypatch: pytest.MonkeyPatch) -> None:
    def fake_status_rows(base_id: str, table: str, *, prefix: str = "", limit: int = 500, timeout_seconds: int = 30):
        assert table == "ConfigCASSave"
        return [
            {"FileName": "changed.0", "PartNo": 0, "DataSize": 1, "BinaryBytes": 1, "BinarySHA1": "b" * 40},
            {"FileName": "same.0", "PartNo": 0, "DataSize": 1, "BinaryBytes": 1, "BinarySHA1": "c" * 40},
            {"FileName": "only.0", "PartNo": 0, "DataSize": 1, "BinaryBytes": 1, "BinarySHA1": "d" * 40},
        ], {"database": "upo_test"}, None

    def fake_row_details(base_id: str, table: str, file_names: list[str], *, timeout_seconds: int = 30):
        assert table == "ConfigCAS"
        return [
            {"FileName": "changed.0", "PartNo": 0, "DataSize": 1, "BinaryBytes": 1, "BinarySHA1": "a" * 40},
            {"FileName": "same.0", "PartNo": 0, "DataSize": 1, "BinaryBytes": 1, "BinarySHA1": "c" * 40},
        ], {"database": "upo_test"}, None

    monkeypatch.setattr(adapter_server, "saved_state_status_rows", fake_status_rows)
    monkeypatch.setattr(adapter_server, "saved_state_copy_row_details", fake_row_details)

    result = adapter_server.metadata_saved_state_status({"base_id": "upo_test", "table": "ConfigCASSave", "include_storage": True})

    assert result["schema"] == "onec_saved_state_status.v1"
    assert result["status"] == "changed"
    assert result["freshness"]["status"] == "live_sql_verified"
    assert result["counts"]["saved_files"] == 3
    assert result["counts"]["changed_files"] == 1
    assert result["counts"]["unchanged_files"] == 1
    assert result["counts"]["saved_only_files"] == 1
    statuses = {item["file_name"]: item["status"] for item in result["files"]}
    assert statuses == {"changed.0": "changed", "only.0": "saved_only", "same.0": "unchanged"}
    assert result["files"][0]["diff_selector"]["method"] == "metadata.saved_state.diff"


def test_saved_state_status_reports_empty(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "saved_state_status_rows", lambda *args, **kwargs: ([], {"database": "upo_test"}, None))
    monkeypatch.setattr(adapter_server, "saved_state_copy_row_details", lambda *args, **kwargs: ([], {"database": "upo_test"}, None))

    result = adapter_server.metadata_saved_state_status({"base_id": "upo_test", "table": "ConfigSave", "include_storage": True})

    assert result["status"] == "empty"
    assert result["current_state"] == {"source": "active", "activation_state": "active"}
    assert result["counts"]["saved_rows"] == 0
    assert result["files"] == []


def test_saved_state_status_default_is_semantic_and_hides_storage(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, str] = {}

    def fake_status_rows(base_id: str, table: str, *, prefix: str = "", limit: int = 500, timeout_seconds: int = 30):
        seen["saved_table"] = table
        return [
            {"FileName": "internal-guid.0", "PartNo": 0, "DataSize": 1, "BinaryBytes": 1, "BinarySHA1": "b" * 40},
        ], {"database": "upo_test"}, None

    def fake_row_details(base_id: str, table: str, file_names: list[str], *, timeout_seconds: int = 30):
        seen["active_table"] = table
        return [
            {"FileName": "internal-guid.0", "PartNo": 0, "DataSize": 1, "BinaryBytes": 1, "BinarySHA1": "a" * 40},
        ], {"database": "upo_test"}, None

    monkeypatch.setattr(adapter_server, "saved_state_status_rows", fake_status_rows)
    monkeypatch.setattr(adapter_server, "saved_state_copy_row_details", fake_row_details)

    result = adapter_server.metadata_saved_state_status(
        {"base_id": "upo_test", "layer": "extension_saved_state"}
    )

    assert seen == {"saved_table": "ConfigCASSave", "active_table": "ConfigCAS"}
    assert result["source"] == {"kind": "saved_state"}
    assert result["query"]["layer"] == "extension_saved_state"
    assert result["layer"] == {
        "name": "extension_saved_state",
        "active_source": "extension_configuration",
    }
    assert result["counts"] == {
        "saved_records": 1,
        "saved_objects": 1,
        "active_records": 1,
        "changed_objects": 1,
        "unchanged_objects": 0,
        "saved_only_objects": 0,
        "changed_parts": 2,
    }
    serialized = json.dumps(result, ensure_ascii=False)
    assert "ConfigCASSave" not in serialized
    assert "ConfigCAS" not in serialized
    assert "internal-guid.0" not in serialized
    assert "BinarySHA1" not in serialized
    assert "database" not in serialized
    assert "files" not in result


def test_saved_state_status_rejects_conflicting_semantic_and_storage_layers() -> None:
    result = adapter_server.metadata_saved_state_status(
        {
            "base_id": "upo_test",
            "layer": "base_saved_state",
            "table": "ConfigCASSave",
        }
    )

    assert result["status"] == "invalid_argument"
    assert result["argument"] == "layer"


def test_configuration_activation_status_aggregates_live_layers_without_cache(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    seen: list[dict[str, Any]] = []

    def fake_status(payload: dict[str, Any]) -> dict[str, Any]:
        seen.append(payload)
        changed = payload["layer"] == "extension_saved_state"
        return {
            "schema": "onec_saved_state_status.v1",
            "method": "metadata.saved_state.status",
            "status": "changed" if changed else "unchanged",
            "counts": {
                "saved_records": 3 if changed else 1,
                "changed_objects": 1 if changed else 0,
                "saved_only_objects": 1 if changed else 0,
                "changed_parts": 4 if changed else 0,
            },
            "freshness": {"status": "live_verified"},
        }

    monkeypatch.setattr(adapter_server, "metadata_saved_state_status", fake_status)

    result = adapter_server.call_method(
        "configuration.activation.status",
        {"base_id": "upo_test"},
    )

    assert [item["layer"] for item in result["layers"]] == [
        "base_saved_state",
        "extension_saved_state",
    ]
    assert result["status"] == "activation_required"
    assert result["activation_required"] is True
    assert result["counts"] == {
        "changed_objects": 1,
        "saved_only_objects": 1,
        "pending_objects": 2,
        "changed_parts": 4,
    }
    assert result["freshness"] == {
        "source": "live_sql",
        "status": "live_verified",
        "uses_cache": False,
        "uses_vector_index": False,
        "scan_complete": True,
    }
    assert result["execution"]["performed"] is False
    assert all(call["include_storage"] is False for call in seen)
    assert all(call["include_unchanged"] is False for call in seen)


def test_configuration_activation_status_is_conservative_when_scan_hits_limit(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_status",
        lambda payload: {
            "schema": "onec_saved_state_status.v1",
            "method": "metadata.saved_state.status",
            "status": "unchanged",
            "counts": {
                "saved_records": payload["limit"],
                "changed_objects": 0,
                "saved_only_objects": 0,
                "changed_parts": 0,
            },
            "freshness": {"status": "live_verified"},
        },
    )

    result = adapter_server.call_method(
        "configuration.activation.status",
        {
            "base_id": "upo_test",
            "layer": "extension_saved_state",
            "limit": 10,
        },
    )

    assert result["status"] == "inconclusive"
    assert result["activation_required"] is None
    assert result["freshness"]["scan_complete"] is False
    assert result["layers"][0]["scan_complete"] is False


def test_configuration_activation_plan_returns_manual_handoff_and_verification(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setattr(
        adapter_server,
        "configuration_activation_status",
        lambda payload: {
            "schema": "onec_configuration_activation_status.v1",
            "method": "configuration.activation.status",
            "status": "activation_required",
            "base_id": payload["base_id"],
            "activation_required": True,
            "scope": {
                "layer": payload.get("layer") or "all",
                "layers_checked": ["extension_saved_state"],
            },
        },
    )
    monkeypatch.setattr(adapter_server.repository_control, "external_1c_enabled", lambda: True)

    result = adapter_server.call_method(
        "configuration.activation.plan",
        {
            "base_id": "upo_test",
            "layer": "extension_saved_state",
        },
    )

    assert result["status"] == "manual_activation_required"
    assert result["next_step"]["kind"] == "manual_1c_designer_action"
    assert result["next_step"]["confirmation_required"] is True
    assert result["review_call"]["method"] == "metadata.saved_state.changes.list"
    assert result["review_call"]["params"]["layer"] == "extension_saved_state"
    assert result["verification_call"] == {
        "method": "configuration.activation.status",
        "params": {
            "base_id": "upo_test",
            "layer": "extension_saved_state",
            "limit": 5000,
        },
        "expected_status_after_activation": "up_to_date",
    }
    assert result["bridge"]["external_1c_enabled"] is True
    assert result["bridge"]["activation_execution_available"] is False
    assert result["execution"]["performed"] is False
    assert result["execution"]["allowed"] is False


def test_configuration_activation_state_migrates_legacy_json_to_local_sqlite_once(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    legacy_path = tmp_path / "activation-requests.json"
    state_db = tmp_path / "adapter-state.sqlite"
    request_id = "actreq-" + "1" * 32
    legacy_path.write_text(
        json.dumps(
            {
                "schema": "onec_configuration_activation_requests.v1",
                "requests": {
                    request_id: {
                        "request_id": request_id,
                        "base_id": "upo_test",
                        "layer": "extension_saved_state",
                        "fingerprint": "a" * 64,
                        "status": "pending",
                        "created_at": 100.0,
                        "expires_at": 1000.0,
                        "events": [
                            {
                                "event": "requested",
                                "at": 100.0,
                            }
                        ],
                    }
                },
            },
            ensure_ascii=False,
        ),
        encoding="utf-8",
    )
    monkeypatch.setenv("ONEC_CONFIGURATION_ACTIVATION_STATE_FILE", str(legacy_path))
    monkeypatch.setenv("ONEC_ADAPTER_STATE_DB", str(state_db))

    migrated = adapter_server.configuration_activation_read_state()

    assert migrated["requests"][request_id]["status"] == "pending"
    assert migrated["requests"][request_id]["events"] == [
        {"event": "requested", "at": 100.0}
    ]
    with sqlite3.connect(state_db) as conn:
        request_row = conn.execute(
            "SELECT base_id, status FROM configuration_activation_requests "
            "WHERE request_id=?",
            (request_id,),
        ).fetchone()
        event_row = conn.execute(
            "SELECT event FROM configuration_activation_events WHERE request_id=?",
            (request_id,),
        ).fetchone()
        migration_row = conn.execute(
            "SELECT value FROM adapter_state_meta "
            "WHERE key='legacy_activation_requests_imported'"
        ).fetchone()
    assert request_row == ("upo_test", "pending")
    assert event_row == ("requested",)
    assert migration_row == ("yes",)

    legacy_path.write_text(
        json.dumps({"requests": {request_id: {**migrated["requests"][request_id], "status": "cancelled"}}}),
        encoding="utf-8",
    )
    reloaded = adapter_server.configuration_activation_read_state()
    assert reloaded["requests"][request_id]["status"] == "pending"


def test_configuration_activation_request_and_debug_execute_are_fingerprinted(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    state_path = tmp_path / "activation-requests.json"
    monkeypatch.setenv("ONEC_CONFIGURATION_ACTIVATION_STATE_FILE", str(state_path))
    state_db = tmp_path / "adapter-state.sqlite"
    monkeypatch.setenv("ONEC_ADAPTER_STATE_DB", str(state_db))
    live_sha1 = {"value": "a" * 40}

    def fake_status(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "schema": "onec_configuration_activation_status.v1",
            "method": "configuration.activation.status",
            "status": "activation_required",
            "base_id": payload["base_id"],
            "activation_required": True,
            "scope": {
                "layer": payload.get("layer") or "all",
                "layers_checked": ["extension_saved_state"],
            },
            "counts": {
                "changed_objects": 1,
                "saved_only_objects": 0,
                "pending_objects": 1,
                "changed_parts": 2,
            },
            "layers": [
                {
                    "layer": "extension_saved_state",
                    "status": "activation_required",
                    "counts": {"pending_objects": 1},
                    "storage": {
                        "files": [
                            {
                                "file_name": "module.0",
                                "status": "changed",
                                "changed_parts": 2,
                                "saved_sha1": [live_sha1["value"]],
                                "active_sha1": ["b" * 40],
                            }
                        ]
                    },
                }
            ],
            "freshness": {
                "source": "live_sql",
                "status": "live_verified",
                "uses_cache": False,
                "uses_vector_index": False,
                "scan_complete": True,
            },
            "execution": {
                "performed": False,
                "designer_started": False,
                "active_configuration_changed": False,
            },
        }

    monkeypatch.setattr(adapter_server, "configuration_activation_status", fake_status)
    monkeypatch.setattr(
        adapter_server.repository_control,
        "_run_designer",
        lambda *args, **kwargs: pytest.fail("Designer must not run in debug mode"),
    )

    requested = adapter_server.call_method(
        "configuration.activation.request",
        {
            "base_id": "upo_test",
            "layer": "extension_saved_state",
            "ttl_seconds": 600,
        },
    )

    assert requested["status"] == "pending"
    assert requested["request_id"].startswith("actreq-")
    assert len(requested["fingerprint"]) == 64
    assert requested["next_call"]["method"] == "configuration.activation.execute"
    assert requested["next_call"]["params"]["mode"] == "debug"
    assert "storage" not in json.dumps(requested, ensure_ascii=False)
    assert state_db.exists()
    assert not state_path.exists()

    confirmation_required = adapter_server.call_method(
        "configuration.activation.execute",
        {
            "base_id": "upo_test",
            "request_id": requested["request_id"],
            "mode": "debug",
            "confirm_activation": False,
        },
    )
    assert confirmation_required["status"] == "confirmation_required"
    assert confirmation_required["execution"]["performed"] is False

    accepted = adapter_server.call_method(
        "configuration.activation.execute",
        {
            "base_id": "upo_test",
            "request_id": requested["request_id"],
            "mode": "debug",
            "confirm_activation": True,
        },
    )

    assert accepted["status"] == "debug_accepted"
    assert accepted["command_preview"] == {
        "runner": "1c_designer",
        "mode": "debug",
        "operation": "manual_extension_database_configuration_update",
        "layer": "extension_saved_state",
        "executable": False,
        "reason": "extension_activation_cli_not_implemented",
    }
    assert accepted["execution"] == {
        "mode": "debug",
        "performed": False,
        "designer_started": False,
        "active_configuration_changed": False,
    }
    request_status = adapter_server.call_method(
        "configuration.activation.request.status",
        {
            "base_id": "upo_test",
            "request_id": requested["request_id"],
        },
    )
    assert request_status["status"] == "debug_accepted"
    assert request_status["fingerprint"] == requested["fingerprint"]
    confirmation_required_cancel = adapter_server.call_method(
        "configuration.activation.request.cancel",
        {
            "base_id": "upo_test",
            "request_id": requested["request_id"],
            "confirm_cancel": False,
        },
    )
    assert confirmation_required_cancel["status"] == "confirmation_required"
    cancelled = adapter_server.call_method(
        "configuration.activation.request.cancel",
        {
            "base_id": "upo_test",
            "request_id": requested["request_id"],
            "confirm_cancel": True,
        },
    )
    assert cancelled["status"] == "cancelled"
    assert cancelled["previous_status"] == "debug_accepted"
    assert cancelled["execution"]["performed"] is False
    audit = adapter_server.call_method(
        "configuration.activation.audit",
        {"base_id": "upo_test"},
    )
    assert audit["status"] == "ok"
    assert audit["count"] == 1
    assert audit["status_counts"] == {"cancelled": 1}
    assert [event["event"] for event in audit["requests"][0]["events"]] == [
        "requested",
        "debug_accepted",
        "cancelled",
    ]
    serialized_audit = json.dumps(audit, ensure_ascii=False)
    assert requested["fingerprint"] not in serialized_audit
    assert "module.0" not in serialized_audit


def test_configuration_activation_debug_execute_blocks_changed_fingerprint(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    monkeypatch.setenv(
        "ONEC_CONFIGURATION_ACTIVATION_STATE_FILE",
        str(tmp_path / "activation-requests.json"),
    )
    monkeypatch.setenv("ONEC_ADAPTER_STATE_DB", str(tmp_path / "adapter-state.sqlite"))
    live_sha1 = {"value": "a" * 40}

    def fake_status(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "schema": "onec_configuration_activation_status.v1",
            "method": "configuration.activation.status",
            "status": "activation_required",
            "base_id": payload["base_id"],
            "activation_required": True,
            "scope": {"layer": payload.get("layer") or "all"},
            "counts": {"pending_objects": 1},
            "layers": [
                {
                    "layer": "base_saved_state",
                    "status": "activation_required",
                    "counts": {"pending_objects": 1},
                    "storage": {
                        "files": [
                            {
                                "file_name": "root",
                                "status": "changed",
                                "saved_sha1": [live_sha1["value"]],
                                "active_sha1": ["b" * 40],
                            }
                        ]
                    },
                }
            ],
        }

    monkeypatch.setattr(adapter_server, "configuration_activation_status", fake_status)
    requested = adapter_server.call_method(
        "configuration.activation.request",
        {"base_id": "upo_test", "layer": "base_saved_state"},
    )
    live_sha1["value"] = "c" * 40

    result = adapter_server.call_method(
        "configuration.activation.execute",
        {
            "base_id": "upo_test",
            "request_id": requested["request_id"],
            "mode": "debug",
            "confirm_activation": True,
        },
    )

    assert result["status"] == "blocked"
    assert result["error"] == "activation_fingerprint_changed"
    assert result["expected_fingerprint"] != result["current_fingerprint"]
    assert result["next_call"]["method"] == "configuration.activation.request"
    assert result["execution"]["performed"] is False


def test_configuration_activation_execute_can_require_runner_debug_receipt(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    monkeypatch.setenv(
        "ONEC_CONFIGURATION_ACTIVATION_STATE_FILE",
        str(tmp_path / "activation-requests.json"),
    )
    monkeypatch.setenv("ONEC_ADAPTER_STATE_DB", str(tmp_path / "adapter-state.sqlite"))

    def fake_status(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "schema": "onec_configuration_activation_status.v1",
            "method": "configuration.activation.status",
            "status": "activation_required",
            "base_id": payload["base_id"],
            "activation_required": True,
            "scope": {"layer": payload.get("layer") or "all"},
            "counts": {"pending_objects": 1},
            "layers": [
                {
                    "layer": "base_saved_state",
                    "status": "activation_required",
                    "counts": {"pending_objects": 1},
                    "storage": {
                        "files": [
                            {
                                "file_name": "root",
                                "status": "changed",
                                "saved_sha1": ["a" * 40],
                                "active_sha1": ["b" * 40],
                            }
                        ]
                    },
                }
            ],
        }

    monkeypatch.setattr(adapter_server, "configuration_activation_status", fake_status)
    monkeypatch.setattr(
        adapter_server.repository_control,
        "repository_config",
        lambda base_id, layer_id="base": (
            {"runner": {"kind": "http", "url": "http://runner:8121"}},
            None,
        ),
    )
    bridge_calls = []

    def fake_bridge(base_id, config, *, layer, timeout_seconds, request_id="", fingerprint=""):
        bridge_calls.append(
            {
                "base_id": base_id,
                "layer": layer,
                "timeout_seconds": timeout_seconds,
                "request_id": request_id,
                "fingerprint": fingerprint,
            }
        )
        return {
            "status": "ready",
            "runner": {"kind": "local"},
            "debug_acceptance": {
                "accepted": True,
                "request_id": request_id,
                "fingerprint": fingerprint,
                "receipt": "c" * 64,
            },
            "execution": {
                "performed": False,
                "designer_started": False,
            },
        }

    monkeypatch.setattr(
        adapter_server.repository_control,
        "activation_debug_probe",
        fake_bridge,
    )
    monkeypatch.setattr(
        adapter_server.repository_control,
        "_run_designer",
        lambda *args, **kwargs: pytest.fail("Designer must not run during bridge debug acceptance"),
    )
    requested = adapter_server.call_method(
        "configuration.activation.request",
        {"base_id": "upo_test", "layer": "base_saved_state"},
    )

    result = adapter_server.call_method(
        "configuration.activation.execute",
        {
            "base_id": "upo_test",
            "request_id": requested["request_id"],
            "mode": "debug",
            "confirm_activation": True,
            "bridge_debug": True,
            "timeout_seconds": 12,
        },
    )

    assert result["status"] == "debug_accepted"
    assert result["bridge_debug"] == {
        "status": "accepted",
        "runner_kind": "local",
        "request_id": requested["request_id"],
        "fingerprint": requested["fingerprint"],
        "receipt": "c" * 64,
        "execution_performed": False,
        "designer_started": False,
    }
    assert result["verification_call"] == {
        "method": "configuration.activation.verify",
        "params": {
            "base_id": "upo_test",
            "request_id": requested["request_id"],
        },
        "expected_status_after_real_activation": "verified_up_to_date",
    }
    assert bridge_calls == [
        {
            "base_id": "upo_test",
            "layer": "base_saved_state",
            "timeout_seconds": 12,
            "request_id": requested["request_id"],
            "fingerprint": requested["fingerprint"],
        }
    ]
    audit = adapter_server.call_method(
        "configuration.activation.audit",
        {"base_id": "upo_test"},
    )
    assert [event["event"] for event in audit["requests"][0]["events"]] == [
        "requested",
        "debug_accepted",
        "bridge_debug_accepted",
    ]
    assert "c" * 64 not in json.dumps(audit, ensure_ascii=False)


def test_configuration_activation_execute_blocks_mismatched_bridge_receipt(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    monkeypatch.setenv(
        "ONEC_CONFIGURATION_ACTIVATION_STATE_FILE",
        str(tmp_path / "activation-requests.json"),
    )
    monkeypatch.setenv("ONEC_ADAPTER_STATE_DB", str(tmp_path / "adapter-state.sqlite"))
    monkeypatch.setattr(
        adapter_server,
        "configuration_activation_status",
        lambda payload: {
            "schema": "onec_configuration_activation_status.v1",
            "method": "configuration.activation.status",
            "status": "activation_required",
            "base_id": payload["base_id"],
            "activation_required": True,
            "scope": {"layer": payload.get("layer") or "all"},
            "counts": {"pending_objects": 1},
            "layers": [
                {
                    "layer": "base_saved_state",
                    "status": "activation_required",
                    "counts": {"pending_objects": 1},
                    "storage": {
                        "files": [
                            {
                                "file_name": "root",
                                "status": "changed",
                                "saved_sha1": ["a" * 40],
                                "active_sha1": ["b" * 40],
                            }
                        ]
                    },
                }
            ],
        },
    )
    monkeypatch.setattr(
        adapter_server.repository_control,
        "repository_config",
        lambda *args, **kwargs: ({"runner": {"kind": "http"}}, None),
    )
    monkeypatch.setattr(
        adapter_server.repository_control,
        "activation_debug_probe",
        lambda base_id, config, **kwargs: {
            "status": "ready",
            "runner": {"kind": "local"},
            "debug_acceptance": {
                "accepted": True,
                "request_id": kwargs["request_id"],
                "fingerprint": "f" * 64,
                "receipt": "c" * 64,
            },
        },
    )
    requested = adapter_server.call_method(
        "configuration.activation.request",
        {"base_id": "upo_test", "layer": "base_saved_state"},
    )

    result = adapter_server.call_method(
        "configuration.activation.execute",
        {
            "base_id": "upo_test",
            "request_id": requested["request_id"],
            "confirm_activation": True,
            "bridge_debug": True,
        },
    )

    assert result["status"] == "blocked"
    assert result["error"] == "activation_bridge_debug_not_accepted"
    assert result["bridge"]["accepted"] is True
    assert result["bridge"]["receipt_valid"] is False
    request_status = adapter_server.call_method(
        "configuration.activation.request.status",
        {"base_id": "upo_test", "request_id": requested["request_id"]},
    )
    assert request_status["status"] == "pending"


def test_configuration_activation_verify_distinguishes_pending_and_aligned_state(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    monkeypatch.setenv(
        "ONEC_CONFIGURATION_ACTIVATION_STATE_FILE",
        str(tmp_path / "activation-requests.json"),
    )
    monkeypatch.setenv("ONEC_ADAPTER_STATE_DB", str(tmp_path / "adapter-state.sqlite"))
    phase = {"value": "pending"}

    def fake_status(payload: dict[str, Any]) -> dict[str, Any]:
        if phase["value"] == "aligned":
            return {
                "schema": "onec_configuration_activation_status.v1",
                "method": "configuration.activation.status",
                "status": "up_to_date",
                "base_id": payload["base_id"],
                "activation_required": False,
                "scope": {"layer": payload.get("layer") or "all"},
                "counts": {"pending_objects": 0},
                "layers": [],
                "freshness": {
                    "source": "live_sql",
                    "status": "live_verified",
                    "uses_cache": False,
                },
            }
        return {
            "schema": "onec_configuration_activation_status.v1",
            "method": "configuration.activation.status",
            "status": "activation_required",
            "base_id": payload["base_id"],
            "activation_required": True,
            "scope": {"layer": payload.get("layer") or "all"},
            "counts": {"pending_objects": 1},
            "layers": [
                {
                    "layer": "extension_saved_state",
                    "status": "activation_required",
                    "counts": {"pending_objects": 1},
                    "storage": {
                        "files": [
                            {
                                "file_name": "module.0",
                                "status": "changed",
                                "saved_sha1": ["a" * 40],
                                "active_sha1": ["b" * 40],
                            }
                        ]
                    },
                }
            ],
            "freshness": {
                "source": "live_sql",
                "status": "live_verified",
                "uses_cache": False,
            },
        }

    monkeypatch.setattr(adapter_server, "configuration_activation_status", fake_status)
    requested = adapter_server.call_method(
        "configuration.activation.request",
        {"base_id": "upo_test", "layer": "extension_saved_state"},
    )

    pending = adapter_server.call_method(
        "configuration.activation.verify",
        {
            "base_id": "upo_test",
            "request_id": requested["request_id"],
        },
    )

    assert pending["status"] == "not_activated"
    assert pending["next_step"]["action"] == "update_database_configuration"
    assert pending["retry_call"]["method"] == "configuration.activation.verify"
    phase["value"] = "aligned"

    verified = adapter_server.call_method(
        "configuration.activation.verify",
        {
            "base_id": "upo_test",
            "request_id": requested["request_id"],
        },
    )

    assert verified["status"] == "verified_up_to_date"
    assert verified["outcome"] == {
        "saved_and_active_aligned": True,
        "activation_proven": False,
        "reason": (
            "Live SQL proves that saved and active layers align. It cannot distinguish "
            "Designer activation from another operation that removed or replaced saved changes."
        ),
    }
    assert verified["execution"]["performed"] is False
    request_status = adapter_server.call_method(
        "configuration.activation.request.status",
        {"base_id": "upo_test", "request_id": requested["request_id"]},
    )
    assert request_status["status"] == "verified_up_to_date"
    audit = adapter_server.call_method(
        "configuration.activation.audit",
        {"base_id": "upo_test"},
    )
    assert [event["event"] for event in audit["requests"][0]["events"]] == [
        "requested",
        "verification_not_activated",
        "verified_up_to_date",
    ]


def test_configuration_activation_verify_marks_changed_fingerprint(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    monkeypatch.setenv(
        "ONEC_CONFIGURATION_ACTIVATION_STATE_FILE",
        str(tmp_path / "activation-requests.json"),
    )
    monkeypatch.setenv("ONEC_ADAPTER_STATE_DB", str(tmp_path / "adapter-state.sqlite"))
    saved_sha1 = {"value": "a" * 40}

    def fake_status(payload: dict[str, Any]) -> dict[str, Any]:
        return {
            "schema": "onec_configuration_activation_status.v1",
            "method": "configuration.activation.status",
            "status": "activation_required",
            "base_id": payload["base_id"],
            "activation_required": True,
            "scope": {"layer": payload.get("layer") or "all"},
            "counts": {"pending_objects": 1},
            "layers": [
                {
                    "layer": "base_saved_state",
                    "status": "activation_required",
                    "counts": {"pending_objects": 1},
                    "storage": {
                        "files": [
                            {
                                "file_name": "root",
                                "status": "changed",
                                "saved_sha1": [saved_sha1["value"]],
                                "active_sha1": ["b" * 40],
                            }
                        ]
                    },
                }
            ],
        }

    monkeypatch.setattr(adapter_server, "configuration_activation_status", fake_status)
    requested = adapter_server.call_method(
        "configuration.activation.request",
        {"base_id": "upo_test", "layer": "base_saved_state"},
    )
    saved_sha1["value"] = "c" * 40

    result = adapter_server.call_method(
        "configuration.activation.verify",
        {
            "base_id": "upo_test",
            "request_id": requested["request_id"],
        },
    )

    assert result["status"] == "changed_since_request"
    assert result["next_call"] == {
        "method": "configuration.activation.request",
        "params": {"base_id": "upo_test", "layer": "base_saved_state"},
    }
    request_status = adapter_server.call_method(
        "configuration.activation.request.status",
        {"base_id": "upo_test", "request_id": requested["request_id"]},
    )
    assert request_status["status"] == "changed_since_request"


def test_configuration_activation_execute_rejects_real_mode() -> None:
    result = adapter_server.call_method(
        "configuration.activation.execute",
        {
            "base_id": "upo_test",
            "request_id": "actreq-test",
            "mode": "apply",
            "confirm_activation": True,
        },
    )

    assert result["status"] == "invalid_argument"
    assert result["argument"] == "mode"
    assert result["allowed_values"] == ["debug"]


def test_configuration_activation_capabilities_hide_runner_details(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setenv("ONEC_ADAPTER_ENABLE_EXTERNAL_1C", "true")
    monkeypatch.setattr(
        adapter_server.repository_control,
        "repository_config",
        lambda base_id, layer_id="base": (
            {
                "designer_path": r"C:\Secret\1cv8.exe",
                "infobase": {"server": "secret-server/secret-base"},
                "infobase_user": "secret-user",
                "infobase_password_env": "SECRET_PASSWORD_ENV",
                "runner": {
                    "kind": "http",
                    "url": "http://secret-runner:8121",
                    "token_env": "SECRET_TOKEN_ENV",
                },
            },
            None,
        ),
    )

    result = adapter_server.call_method(
        "configuration.activation.capabilities",
        {"base_id": "upo_test"},
    )

    assert result["status"] == "ok"
    assert result["workflow"]["debug_acceptance"] is True
    assert result["workflow"]["real_execution"] is False
    assert result["layers"]["base_saved_state"]["designer_operation"] == "/UpdateDBCfg"
    assert result["layers"]["extension_saved_state"]["status"] == "manual_only"
    assert result["runner"] == {
        "configured": True,
        "kind": "http",
        "ready_for_future_base_activation": True,
        "external_1c_enabled": True,
        "designer_path_configured": True,
        "infobase_selector_configured": True,
        "problem": None,
    }
    serialized = json.dumps(result, ensure_ascii=False)
    for secret_fragment in (
        "Secret",
        "secret-server",
        "secret-base",
        "secret-user",
        "SECRET_PASSWORD_ENV",
        "secret-runner",
        "SECRET_TOKEN_ENV",
    ):
        assert secret_fragment not in serialized


def test_configuration_activation_bridge_probe_checks_local_runner_without_execution(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    designer = tmp_path / "1cv8.exe"
    designer.write_bytes(b"test")
    monkeypatch.setattr(
        adapter_server.repository_control,
        "repository_config",
        lambda base_id, layer_id="base": (
            {
                "designer_path": str(designer),
                "infobase": {"server": "secret-server/secret-base"},
                "runner": {"kind": "local"},
            },
            None,
        ),
    )
    monkeypatch.setattr(
        adapter_server.repository_control,
        "_run_designer",
        lambda *args, **kwargs: pytest.fail("Designer must not run during bridge probe"),
    )

    result = adapter_server.call_method(
        "configuration.activation.bridge.probe",
        {
            "base_id": "upo_test",
            "layer": "base_saved_state",
        },
    )

    assert result["status"] == "ready"
    assert result["runner"] == {
        "kind": "local",
        "reachable": True,
        "designer_path_configured": True,
        "designer_available": True,
        "infobase_selector_configured": True,
    }
    assert result["operation"] == {
        "kind": "/UpdateDBCfg",
        "execution_supported": False,
        "extension_manual_only": False,
    }
    assert result["execution"]["performed"] is False
    serialized = json.dumps(result, ensure_ascii=False)
    assert str(designer) not in serialized
    assert "secret-server" not in serialized
    assert "secret-base" not in serialized


def test_configuration_activation_bridge_probe_sanitizes_http_runner_response(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setattr(
        adapter_server.repository_control,
        "repository_config",
        lambda base_id, layer_id="base": (
            {
                "runner": {
                    "kind": "http",
                    "url": "http://secret-runner:8121",
                    "token_env": "SECRET_TOKEN",
                }
            },
            None,
        ),
    )
    monkeypatch.setattr(
        adapter_server.repository_control,
        "activation_debug_probe",
        lambda *args, **kwargs: {
            "status": "ready",
            "runner": {
                "kind": "http",
                "reachable": True,
                "designer_available": True,
                "secret_url": "http://must-not-leak",
            },
            "operation": {
                "kind": None,
                "execution_supported": False,
                "extension_manual_only": True,
                "raw_command": "must-not-leak",
            },
            "path": r"C:\must-not-leak\1cv8.exe",
        },
    )

    result = adapter_server.call_method(
        "configuration.activation.bridge.probe",
        {
            "base_id": "upo_test",
            "layer": "extension_saved_state",
        },
    )

    assert result["status"] == "ready"
    assert result["runner"] == {
        "kind": "http",
        "reachable": True,
        "designer_available": True,
    }
    assert result["operation"] == {
        "kind": None,
        "execution_supported": False,
        "extension_manual_only": True,
    }
    serialized = json.dumps(result, ensure_ascii=False)
    assert "must-not-leak" not in serialized
    assert "secret-runner" not in serialized
    assert "SECRET_TOKEN" not in serialized


def test_configuration_activation_methods_are_registered_and_validate_layer() -> None:
    methods = {
        row["name"]: row
        for row in adapter_server.call_method("help.methods", {})["methods"]
    }

    assert methods["configuration.activation.status"]["input_schema"]["required"] == ["base_id"]
    assert methods["configuration.activation.plan"]["http_routes"] == [{"verb": "POST"}]
    assert methods["configuration.activation.request"]["input_schema"]["properties"]["ttl_seconds"]["minimum"] == 60
    assert methods["configuration.activation.request.cancel"]["input_schema"]["properties"]["confirm_cancel"]["const"] is True
    assert methods["configuration.activation.audit"]["input_schema"]["required"] == ["base_id"]
    assert methods["configuration.activation.capabilities"]["input_schema"]["required"] == ["base_id"]
    assert methods["configuration.activation.bridge.probe"]["input_schema"]["properties"]["timeout_seconds"]["maximum"] == 60
    assert methods["configuration.activation.execute"]["input_schema"]["properties"]["mode"]["enum"] == ["debug"]
    assert methods["configuration.activation.execute"]["input_schema"]["properties"]["bridge_debug"]["type"] == "boolean"
    assert methods["configuration.activation.verify"]["input_schema"]["required"] == ["base_id", "request_id"]
    result = adapter_server.call_method(
        "configuration.activation.status",
        {"base_id": "upo_test", "layer": "extension"},
    )
    assert result["status"] == "invalid_argument"
    assert result["argument"] == "layer"


def test_saved_state_changes_list_aggregates_pending_files(monkeypatch: pytest.MonkeyPatch) -> None:
    def fake_status(payload: dict[str, Any]) -> dict[str, Any]:
        table = payload["table"]
        if table == "ConfigSave":
            return {
                "schema": "onec_saved_state_status.v1",
                "method": "metadata.saved_state.status",
                "status": "unchanged",
                "counts": {"changed_files": 0, "saved_only_files": 0, "unchanged_files": 1},
                "files": [],
                "freshness": {"status": "live_sql_verified"},
            }
        return {
            "schema": "onec_saved_state_status.v1",
            "method": "metadata.saved_state.status",
            "status": "changed",
            "counts": {"changed_files": 1, "saved_only_files": 1, "unchanged_files": 0},
            "files": [
                {
                    "file_name": "changed.0",
                    "status": "changed",
                    "changed_parts": 2,
                    "diff_selector": {"method": "metadata.saved_state.diff", "base_id": payload["base_id"], "table": table, "file_name": "changed.0"},
                },
                {
                    "file_name": "only.0",
                    "status": "saved_only",
                    "changed_parts": 1,
                    "diff_selector": {"method": "metadata.saved_state.diff", "base_id": payload["base_id"], "table": table, "file_name": "only.0"},
                },
            ],
            "freshness": {"status": "live_sql_verified"},
        }

    monkeypatch.setattr(adapter_server, "metadata_saved_state_status", fake_status)

    result = adapter_server.metadata_saved_state_changes_list({"base_id": "upo_test", "include_storage": True})

    assert result["schema"] == "onec_saved_state_changes_list.v1"
    assert result["status"] == "changed"
    assert result["freshness"]["status"] == "live_sql_verified"
    assert result["counts"]["tables"] == 2
    assert result["counts"]["changed_files"] == 1
    assert result["counts"]["saved_only_files"] == 1
    assert [(item["table"], item["file_name"], item["status"]) for item in result["files"]] == [
        ("ConfigCASSave", "changed.0", "changed"),
        ("ConfigCASSave", "only.0", "saved_only"),
    ]
    assert result["files"][0]["diff_selector"]["method"] == "metadata.saved_state.diff"


def test_saved_state_changes_list_can_include_context(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_status",
        lambda payload: {
            "schema": "onec_saved_state_status.v1",
            "method": "metadata.saved_state.status",
            "status": "changed",
            "counts": {"changed_files": 1, "saved_only_files": 0, "unchanged_files": 0},
            "files": [
                {
                    "file_name": "changed.0",
                    "status": "changed",
                    "changed_parts": 2,
                    "diff_selector": {"method": "metadata.saved_state.diff", "base_id": payload["base_id"], "table": payload["table"], "file_name": "changed.0"},
                },
            ],
            "freshness": {"status": "live_sql_verified"},
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_change_context",
        lambda **kwargs: {
            "kind": "module",
            "presentation": "ОбщаяФорма.t_Форма.Модуль формы",
            "module": {"kind": "form_module", "name": "Модуль формы"},
            "source": {"method": "metadata.saved_state.modules.search", "status": "resolved_by_file"},
        },
    )

    result = adapter_server.metadata_saved_state_changes_list({"base_id": "upo_test", "table": "ConfigCASSave", "include_context": True})

    assert result["query"]["include_context"] is True
    assert result["changes"][0]["context"]["kind"] == "module"
    assert result["changes"][0]["context"]["presentation"] == "ОбщаяФорма.t_Форма.Модуль формы"
    assert "files" not in result
    assert "tables" not in result


def test_saved_state_changes_list_accepts_semantic_layer(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: list[dict[str, Any]] = []

    def fake_status(payload: dict[str, Any]) -> dict[str, Any]:
        seen.append(payload)
        return {
            "schema": "onec_saved_state_status.v1",
            "method": "metadata.saved_state.status",
            "status": "empty",
            "counts": {
                "saved_rows": 0,
                "saved_files": 0,
                "active_rows": 0,
                "changed_files": 0,
                "unchanged_files": 0,
                "saved_only_files": 0,
                "changed_rows": 0,
            },
            "files": [],
            "freshness": {"status": "live_sql_verified"},
        }

    monkeypatch.setattr(adapter_server, "metadata_saved_state_status", fake_status)

    result = adapter_server.metadata_saved_state_changes_list(
        {"base_id": "upo_test", "layer": "extension_saved_state"}
    )

    assert result["query"]["layers"] == ["extension_saved_state"]
    assert len(seen) == 1
    assert seen[0]["table"] == "ConfigCASSave"
    assert seen[0]["include_storage"] is True


def test_saved_state_changes_list_rejects_conflicting_semantic_and_storage_layers() -> None:
    result = adapter_server.metadata_saved_state_changes_list(
        {
            "base_id": "upo_test",
            "layer": "base_saved_state",
            "table": "ConfigCASSave",
        }
    )

    assert result["status"] == "invalid_argument"
    assert result["argument"] == "layer"


def test_saved_state_changes_list_default_is_name_first(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_status",
        lambda payload: {
            "schema": "onec_saved_state_status.v1",
            "method": "metadata.saved_state.status",
            "status": "changed",
            "counts": {"changed_files": 1, "saved_only_files": 0, "unchanged_files": 0},
            "files": [
                {
                    "file_name": "extension-guid__form-guid.0",
                    "status": "changed",
                    "changed_parts": 2,
                    "saved_sha1": ["saved-sha1"],
                    "active_sha1": ["active-sha1"],
                    "diff_selector": {
                        "method": "metadata.saved_state.diff",
                        "base_id": payload["base_id"],
                        "table": payload["table"],
                        "file_name": "extension-guid__form-guid.0",
                    },
                }
            ],
            "freshness": {"status": "live_sql_verified"},
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_change_context",
        lambda **kwargs: {
            "kind": "module",
            "presentation": "test2.t_Форма.Модуль формы",
            "owner": {
                "status": "resolved",
                "kind": "Catalog",
                "name": "test2",
                "guid": "owner-guid",
                "source": "saved_state_descriptor",
            },
            "form": {"name": "t_Форма", "guid": "form-guid"},
            "module": {"kind": "form_module", "name": "Модуль формы"},
            "streams": [
                {
                    "module_ref": "ConfigCASSave:extension-guid__form-guid.0",
                    "write_plan_target": {"file_name": "extension-guid__form-guid.0"},
                }
            ],
            "source": {"method": "metadata.saved_state.modules.search", "status": "resolved_by_file"},
        },
    )

    result = adapter_server.metadata_saved_state_changes_list(
        {"base_id": "upo_test", "table": "ConfigCASSave"}
    )

    assert result["source"] == {"kind": "saved_state"}
    assert result["query"]["layers"] == ["extension_saved_state"]
    assert result["query"]["context_enrichment"] is True
    assert result["counts"] == {
        "layers": 1,
        "changes": 1,
        "changed": 1,
        "saved_only": 0,
        "unchanged": 0,
        "error_layers": 0,
    }
    assert result["changes"][0]["presentation"] == "test2.t_Форма.Модуль формы"
    assert result["changes"][0]["selector"] == {
        "ref": "Catalog.test2",
        "form": "t_Форма",
        "module": "Модуль формы",
        "qualified_name": "test2.t_Форма.Модуль формы",
    }
    assert result["diagnostics"]["resolved_contexts"] == 1
    assert result["freshness"] == {
        "source": "saved_state",
        "status": "live_verified",
        "verified": True,
    }
    assert result["layers"][0]["freshness"] == {
        "source": "saved_state",
        "status": "live_verified",
        "verified": True,
    }
    assert result["layers"][0]["counts"] == {
        "saved_records": 0,
        "saved_objects": 0,
        "active_records": 0,
        "changed": 1,
        "unchanged": 0,
        "saved_only": 0,
        "changed_parts": 0,
    }
    assert "tables" not in result["counts"]
    assert "files" not in result["counts"]
    for storage_marker in (
        "live_sql",
        "ConfigCASSave",
        "extension-guid",
        "form-guid",
        "owner-guid",
        "file_name",
        "module_ref",
        "write_plan_target",
        "saved_sha1",
        "active_sha1",
        "diff_selector",
    ):
        assert storage_marker not in str(result)


def test_saved_state_change_context_does_not_claim_generic_role_as_resolved(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setattr(adapter_server, "saved_state_form_search_row", lambda **kwargs: {"form": {}})
    monkeypatch.setattr(
        adapter_server,
        "saved_state_module_search_row",
        lambda **kwargs: {
            "module": {"kind": "form_module", "name": "Модуль формы"},
            "qualified_name": "Модуль формы",
            "streams": [],
        },
    )

    context = adapter_server.metadata_saved_state_change_context(
        base_id="upo_test",
        table="ConfigCASSave",
        file_name="opaque.0",
        timeout_seconds=30,
    )

    assert context is None


def test_saved_state_change_context_family_adds_form_name_to_module() -> None:
    files = [
        {
            "table": "ConfigCASSave",
            "file_name": "extension-guid__form-guid",
            "context": {
                "kind": "module",
                "presentation": "test2.Модуль формы",
                "owner": {"kind": "Catalog", "name": "test2"},
                "module": {"kind": "form_module", "name": "Модуль формы"},
            },
        },
        {
            "table": "ConfigCASSave",
            "file_name": "extension-guid__form-guid.0",
            "context": {
                "kind": "form",
                "presentation": "test2.t_Форма.Форма",
                "owner": {"kind": "Catalog", "name": "test2"},
                "form": {"name": "t_Форма"},
            },
        },
    ]

    adapter_server.enrich_saved_state_change_context_families(files)

    assert files[0]["context"]["form"]["name"] == "t_Форма"
    assert files[0]["context"]["presentation"] == "test2.t_Форма.Модуль формы"


def test_public_saved_state_unresolved_group_hides_file_identity() -> None:
    public = adapter_server.public_saved_state_change_group(
        {
            "key": "file|ConfigCASSave|opaque-hash",
            "kind": "file",
            "presentation": "opaque-hash",
            "counts": {"files": 1, "changed_files": 1, "saved_only_files": 0, "unchanged_files": 0},
            "files": [
                {
                    "table": "ConfigCASSave",
                    "file_name": "opaque-hash",
                    "status": "changed",
                    "changed_parts": 2,
                }
            ],
        },
        3,
    )

    assert public["key"] == "unresolved_change:3"
    assert public["kind"] == "unresolved"
    assert public["counts"] == {"changes": 1, "changed": 1, "saved_only": 0, "unchanged": 0}
    assert "presentation" not in public
    assert "selector" not in public
    assert "opaque-hash" not in str(public)
    assert "ConfigCASSave" not in str(public)


def test_saved_state_changes_list_can_group_by_context(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_status",
        lambda payload: {
            "schema": "onec_saved_state_status.v1",
            "method": "metadata.saved_state.status",
            "status": "changed",
            "counts": {"changed_files": 1, "saved_only_files": 1, "unchanged_files": 0},
            "files": [
                {
                    "file_name": "changed.0",
                    "status": "changed",
                    "changed_parts": 2,
                    "diff_selector": {"method": "metadata.saved_state.diff", "base_id": payload["base_id"], "table": payload["table"], "file_name": "changed.0"},
                },
                {
                    "file_name": "only.0",
                    "status": "saved_only",
                    "changed_parts": 1,
                    "diff_selector": {"method": "metadata.saved_state.diff", "base_id": payload["base_id"], "table": payload["table"], "file_name": "only.0"},
                },
            ],
            "freshness": {"status": "live_sql_verified"},
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_change_context",
        lambda **kwargs: {
            "kind": "module",
            "presentation": "ОбщаяФорма.t_Форма.Модуль формы",
            "form": {"name": "t_Форма", "guid": "form-guid"},
            "module": {"kind": "form_module", "name": "Модуль формы"},
            "streams": [
                {
                    "stream_index": 0,
                    "module_ref": "ConfigCASSave:changed.0#stream:0",
                    "write_plan_target": {"table": "ConfigCASSave", "file_name": "changed.0", "module_ref": "ConfigCASSave:changed.0#stream:0"},
                }
            ],
            "source": {"method": "metadata.saved_state.modules.search", "status": "resolved_by_file"},
        },
    )

    result = adapter_server.metadata_saved_state_changes_list(
        {"base_id": "upo_test", "table": "ConfigCASSave", "group_by_context": True, "include_storage": True}
    )

    assert result["query"]["group_by_context"] is True
    assert result["query"]["context_enrichment"] is True
    assert result["counts"]["groups"] == 1
    assert len(result["groups"]) == 1
    assert result["groups"][0]["presentation"] == "ОбщаяФорма.t_Форма.Модуль формы"
    assert result["groups"][0]["counts"] == {"files": 2, "changed_files": 1, "saved_only_files": 1, "unchanged_files": 0}
    assert [item["file_name"] for item in result["groups"][0]["files"]] == ["changed.0", "only.0"]
    assert len(result["groups"][0]["selectors"]["diff"]) == 2
    assert result["groups"][0]["selectors"]["module_refs"] == ["ConfigCASSave:changed.0#stream:0"]
    assert result["groups"][0]["selectors"]["write_plan_targets"][0]["file_name"] == "changed.0"
    assert [action["kind"] for action in result["groups"][0]["next_actions"]] == [
        "inspect_diff",
        "inspect_diff",
        "read_module",
        "preflight_write",
    ]
    assert result["groups"][0]["next_actions"][2]["payload"] == {"base_id": "upo_test", "module_ref": "ConfigCASSave:changed.0#stream:0", "state": "working"}
    assert result["groups"][0]["next_actions"][3]["payload"]["target"]["file_name"] == "changed.0"
    assert result["groups"][0]["action_summary"]["total"] == 4
    assert result["groups"][0]["action_summary"]["by_kind"] == {"inspect_diff": 2, "read_module": 1, "preflight_write": 1}
    assert result["groups"][0]["recommended_next_action"]["kind"] == "inspect_diff"
    assert result["groups"][0]["recommended_next_action"]["method"] == "metadata.saved_state.diff"
    assert result["action_summary"]["total"] == 4
    assert result["action_summary"]["by_kind"] == {"inspect_diff": 2, "read_module": 1, "preflight_write": 1}
    assert result["recommended_next_action"]["group_presentation"] == "ОбщаяФорма.t_Форма.Модуль формы"
    assert result["recommended_next_action"]["action"]["kind"] == "inspect_diff"


def test_saved_state_prepare_apply_requires_allow_flag(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "saved_state_prepare_file_names",
        lambda payload, base_id, source_table, timeout_seconds: (["owner__module.0"], {"guid": "owner"}, None),
    )
    monkeypatch.setattr(
        adapter_server,
        "saved_state_copy_row_details",
        lambda base_id, table, file_names, timeout_seconds=30: (
            ([{"FileName": "owner__module.0", "PartNo": 0, "DataSize": 10, "BinaryBytes": 10, "BinarySHA1": "A" * 40}] if table == "ConfigCAS" else []),
            {"database": "upo_test"},
            None,
        ),
    )

    result = adapter_server.metadata_saved_state_prepare(
        {"base_id": "upo_test", "target_table": "ConfigCASSave", "mode": "apply", "file_name": "owner__module.0"}
    )

    assert result["status"] == "invalid_argument"
    assert result["argument"] == "allow_sql_saved_state_prepare"


def test_saved_state_prepare_blocks_target_collision(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "saved_state_prepare_file_names",
        lambda payload, base_id, source_table, timeout_seconds: (["owner__module.0"], {"guid": "owner"}, None),
    )
    monkeypatch.setattr(
        adapter_server,
        "saved_state_copy_row_details",
        lambda base_id, table, file_names, timeout_seconds=30: (
            [{"FileName": "owner__module.0", "PartNo": 0, "DataSize": 10, "BinaryBytes": 10, "BinarySHA1": "A" * 40}],
            {"database": "upo_test"},
            None,
        ),
    )

    result = adapter_server.metadata_saved_state_prepare(
        {
            "base_id": "upo_test",
            "target_table": "ConfigCASSave",
            "file_name": "owner__module.0",
            "include_storage": True,
        }
    )

    assert result["status"] == "blocked_target_collision"
    assert result["ready_to_copy"] is False
    assert result["counts"]["target_rows"] == 1


def test_metadata_write_preserves_apply_and_verify_mode(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
        seen["payload"] = payload
        return {"schema": "onec_form_element_write_apply.v1", "status": "verified", "applied": True}

    monkeypatch.setattr(adapter_server, "metadata_form_element_write_apply", fake_apply)

    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "target": {"kind": "form", "table": "ConfigCASSave", "file_name": "form-guid.0", "element": "КомандаПример1"},
            "mode": "apply_and_verify",
            "allow_sql_saved_state_apply": True,
            "edits": [{"property": "Заголовок", "value": "ПРОВЕРКА"}],
        }
    )

    assert seen["payload"]["execution_mode"] == "apply_and_verify"
    assert result["execution_mode"] == "apply_and_verify"
    assert result["status"] == "verified"


def test_metadata_write_routes_module_target(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_module_apply(payload: dict[str, Any]) -> dict[str, Any]:
        seen["payload"] = payload
        return {"schema": "onec_module_write_apply.v1", "status": "verified_and_rolled_back", "applied": True, "rolled_back": True}

    monkeypatch.setattr(adapter_server, "metadata_module_write_apply", fake_module_apply)

    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "target": {"kind": "module", "module_ref": "ConfigCASSave:object__module.0#stream:4"},
            "mode": "apply_and_rollback",
            "allow_sql_saved_state_apply": True,
            "allow_sql_saved_state_rollback": True,
            "old": "a",
            "new": "b",
        }
    )

    assert seen["payload"]["execution_mode"] == "apply_and_rollback"
    assert seen["payload"]["allow_saved_state_write"] is True
    assert seen["payload"]["module_ref"] == "ConfigCASSave:object__module.0#stream:4"
    assert result["target_kind"] == "module"
    assert result["routed_method"] == adapter_server.MODULE_WRITE_APPLY_METHOD
    assert result["status"] == "verified_and_rolled_back"


def test_metadata_write_resolves_single_module_target(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_search(payload: dict[str, Any]) -> dict[str, Any]:
        seen["search"] = payload
        return {
            "status": "ok",
            "counts": {"modules": 1, "scanned": 1},
            "modules": [
                {
                    "payload": {"sha1": "old-sha"},
                    "streams": [{"module_ref": "ConfigCASSave:owner__module.0#stream:4"}],
                }
            ],
        }

    def fake_module_apply(payload: dict[str, Any]) -> dict[str, Any]:
        seen["apply"] = payload
        return {"schema": "onec_module_write_apply.v1", "status": "planned"}

    monkeypatch.setattr(adapter_server, "metadata_saved_state_modules_search", fake_search)
    monkeypatch.setattr(adapter_server, "metadata_module_write_apply", fake_module_apply)

    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "target": {"kind": "module", "owner_guid": "owner", "query": "Команда1"},
            "old": "a",
            "new": "b",
        }
    )

    assert seen["search"]["owner_guid"] == "owner"
    assert seen["search"]["query"] == "Команда1"
    assert seen["apply"]["module_ref"] == "ConfigCASSave:owner__module.0#stream:4"
    assert seen["apply"]["expected_sha1"] == "old-sha"
    assert result["resolution"]["method"] == adapter_server.SAVED_STATE_MODULES_SEARCH_METHOD
    assert result["status"] == "planned"


def test_metadata_write_reports_ambiguous_module_resolution(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_modules_search",
        lambda payload: {
            "status": "ok",
            "counts": {"modules": 2, "scanned": 2},
            "modules": [
                {"streams": [{"module_ref": "ConfigCASSave:a__m.0#stream:1"}]},
                {"streams": [{"module_ref": "ConfigCASSave:b__m.0#stream:1"}]},
            ],
        },
    )

    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "target": {"kind": "module", "query": "Команда"},
            "old": "a",
            "new": "b",
        }
    )

    assert result["status"] == "ambiguous"
    assert result["error"] == "module_target_not_resolved"
    assert result["counts"]["stream_matches"] == 2


def test_metadata_write_active_module_ref_requires_saved_state_prepare(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_prepare(payload: dict[str, Any]) -> dict[str, Any]:
        seen["prepare"] = payload
        return {"schema": "onec_saved_state_prepare.v1", "status": "plan_ready", "ready_to_copy": True}

    monkeypatch.setattr(adapter_server, "metadata_saved_state_prepare", fake_prepare)

    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "target": {"kind": "module", "module_ref": "ConfigCAS:owner__module.0#stream:4"},
            "old": "a",
            "new": "b",
        }
    )

    assert result["status"] == "blocked"
    assert result["error"] == "saved_state_prepare_required"
    assert result["prepared_target"] == {"layer": "extension_saved_state"}
    assert "prepared_module_ref" not in result
    assert result["next_resolution"]["method"] == "metadata.saved_state.prepare"
    assert result["next_resolution"]["params"] == {
        "base_id": "upo_test",
        "layer": "extension_saved_state",
        "mode": "plan",
        "selector_required": "Pass a public 1C ref or kind/name selector.",
    }
    assert seen["prepare"]["mode"] == "plan"
    assert seen["prepare"]["source_table"] == "ConfigCAS"
    assert seen["prepare"]["target_table"] == "ConfigCASSave"


def test_metadata_write_active_module_ref_auto_prepares_when_apply_allowed(monkeypatch: pytest.MonkeyPatch) -> None:
    seen: dict[str, Any] = {}

    def fake_prepare(payload: dict[str, Any]) -> dict[str, Any]:
        seen["prepare"] = payload
        return {"schema": "onec_saved_state_prepare.v1", "status": "verified", "applied": True}

    def fake_module_apply(payload: dict[str, Any]) -> dict[str, Any]:
        seen["apply"] = payload
        return {"schema": "onec_module_write_apply.v1", "status": "verified", "applied": True}

    monkeypatch.setattr(adapter_server, "metadata_saved_state_prepare", fake_prepare)
    monkeypatch.setattr(adapter_server, "metadata_module_write_apply", fake_module_apply)

    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "target": {"kind": "module", "module_ref": "ConfigCAS:owner__module.0#stream:4"},
            "mode": "apply_and_verify",
            "allow_sql_saved_state_apply": True,
            "old": "a",
            "new": "b",
        }
    )

    assert seen["prepare"]["mode"] == "apply_and_verify"
    assert seen["prepare"]["allow_sql_saved_state_prepare"] is True
    assert seen["apply"]["module_ref"] == "ConfigCASSave:owner__module.0#stream:4"
    assert result["status"] == "verified"
    assert result["resolution"]["method"] == "metadata.saved_state.prepare"


def test_metadata_write_missing_module_target_exposes_prepare_plan(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(adapter_server, "resolve_write_gate_context", lambda payload: payload)
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_modules_search",
        lambda payload: {"status": "ok", "counts": {"modules": 0}, "modules": []},
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_prepare",
        lambda payload: {
            "schema": "onec_saved_state_prepare.v1",
            "status": "plan_ready",
            "ready_to_copy": True,
            "target": {"table": payload["target_table"]},
        },
    )

    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "target": {"kind": "module", "extension": "test2", "object_type": "CommonForm", "object_name": "t_Форма"},
            "old": "a",
            "new": "b",
        }
    )

    assert result["status"] == "not_found"
    assert result["error"] == "module_target_not_resolved"
    assert result["next_resolution"]["method"] == "metadata.saved_state.prepare"
    assert result["next_resolution"]["params"] == {
        "base_id": "upo_test",
        "layer": "extension_saved_state",
        "ref": "CommonForm.t_Форма",
        "kind": "CommonForm",
        "name": "t_Форма",
        "extension": "test2",
        "mode": "plan",
    }
    assert result["prepare_plan"]["status"] == "plan_ready"


def test_metadata_write_missing_module_target_auto_prepares_and_retries(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: dict[str, int] = {"search": 0}
    seen: dict[str, Any] = {}
    monkeypatch.setattr(
        adapter_server,
        "extension_filter_to_guid",
        lambda base_id, extension, method: (
            "fb26cf42-7609-11f1-828f-005056b0d483",
            None,
        ),
    )

    def fake_search(payload: dict[str, Any]) -> dict[str, Any]:
        calls["search"] += 1
        seen["search"] = payload
        if calls["search"] == 1:
            return {"status": "ok", "counts": {"modules": 0}, "modules": []}
        return {
            "status": "ok",
            "counts": {"modules": 1},
            "modules": [
                {
                    "payload": {"sha1": "payload-sha"},
                    "streams": [{"module_ref": "ConfigCASSave:owner__module.0#stream:0"}],
                }
            ],
        }

    def fake_module_apply(payload: dict[str, Any]) -> dict[str, Any]:
        seen["apply"] = payload
        return {"schema": "onec_module_write_apply.v1", "status": "verified", "applied": True}

    monkeypatch.setattr(adapter_server, "metadata_saved_state_modules_search", fake_search)
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_prepare",
        lambda payload: {"schema": "onec_saved_state_prepare.v1", "status": "verified", "applied": True},
    )
    monkeypatch.setattr(adapter_server, "metadata_module_write_apply", fake_module_apply)

    result = adapter_server.metadata_write(
        {
            "base_id": "upo_test",
            "target": {"kind": "module", "extension": "test2", "object_type": "CommonForm", "object_name": "t_Форма"},
            "mode": "apply_and_verify",
            "allow_sql_saved_state_apply": True,
            "old": "a",
            "new": "b",
        }
    )

    assert calls["search"] == 2
    assert seen["search"]["object_type"] == "CommonForm"
    assert seen["search"]["object_name"] == "t_Форма"
    assert seen["apply"]["module_ref"] == "ConfigCASSave:owner__module.0#stream:0"
    assert seen["apply"]["expected_sha1"] == "payload-sha"
    assert result["status"] == "verified"


def test_metadata_write_retry_resolves_prepared_extension_object_module_by_canonical_name(monkeypatch: pytest.MonkeyPatch) -> None:
    """Prepare/retry remains public even when the generic stream search stays empty."""
    active_hash = "a" * 40
    canonical_name = "8e02accd-8a34-11f1-8294-005056b0d483__e064659b-6bf8-4162-8c3d-ae186c151ff1.0"
    state = {"prepared": False, "searches": 0}

    def fake_prepare(payload: dict[str, Any]) -> dict[str, Any]:
        if payload.get("mode") == "apply_and_verify":
            state["prepared"] = True
            return {"status": "verified", "apply_result": {"prepare_receipt": {"receipt_id": "receipt-1"}}}
        return {"status": "plan_ready", "source_rows": [{"source_file_name": active_hash, "file_name": canonical_name}]}

    def fake_read_module(payload: dict[str, Any]) -> dict[str, Any]:
        if payload["module_ref"] != f"ConfigCASSave:{canonical_name}#stream:4" or not state["prepared"]:
            return {"status": "not_found"}
        return {"status": "ok", "text": "Процедура Проверка()\nКонецПроцедуры", "content_sha1": "b" * 40}

    monkeypatch.setattr(adapter_server, "metadata_saved_state_prepare", fake_prepare)
    monkeypatch.setattr(adapter_server, "metadata_object_modules", lambda _payload: {"status": "ok", "modules": [{"kind": "object_module", "module_id": f"ConfigCAS:{active_hash}#stream:4"}]})
    monkeypatch.setattr(adapter_server, "read_module", fake_read_module)
    monkeypatch.setattr(
        adapter_server,
        "metadata_saved_state_modules_search",
        lambda _payload: {"status": "ok", "modules": []},
    )

    resolved = adapter_server.metadata_write_resolve_module_target(
        {
            "base_id": "upo_test",
            "extension": "фс_Отчеты1",
            "ref": "Report.фс_ПродовольственнаяКорзина",
            "old": "Проверка",
            "new": "Проверка2",
            "allow_sql_saved_state_apply": True,
        },
        {"kind": "module"},
        "apply_and_verify",
    )

    assert isinstance(resolved, tuple)
    write_payload, resolution = resolved
    assert write_payload["module_ref"] == f"ConfigCASSave:{canonical_name}#stream:4"
    assert write_payload["_auto_prepare_receipt_id"] == "receipt-1"
    assert resolution["public_module_resolution"]["status"] == "resolved_public_object_module"


def test_saved_state_apply_requires_explicit_allow_flag() -> None:
    result = adapter_server.storage_saved_state_apply_proposal(
        {
            "base_id": "upo_test",
            "proposal": {
                "source": {"table": "ConfigSave", "file_name": "form-guid.0"},
                "original": {"sha1": "abc"},
                "encoded": {"sha1": "def", "payload_hex": "00"},
            },
        }
    )

    assert result["status"] == "invalid_argument"
    assert result["argument"] == "allow_sql_saved_state_apply"


def test_saved_state_apply_blocks_unsafe_form_payload_rewrite() -> None:
    original = b"old-payload"
    replacement = b"short"
    proposal = {
        "schema": "onec_change_proposal.v1",
        "method": "metadata.form.element.write",
        "status": "accepted_for_review",
        "source": {"table": "ConfigCASSave", "file_name": "form-guid.0"},
        "original": {"sha1": adapter_server.hashlib.sha1(original).hexdigest(), "bytes": len(original)},
        "encoded": {
            "sha1": adapter_server.hashlib.sha1(replacement).hexdigest(),
            "bytes": len(replacement),
            "payload_hex": replacement.hex(),
        },
        "validation": {"mode": "path", "status": "ok"},
    }

    result = adapter_server.storage_saved_state_apply_proposal(
        {"base_id": "upo_test", "allow_sql_saved_state_apply": True, "proposal": proposal}
    )

    assert result["status"] == "unsafe_form_payload_rewrite"
    assert result["applied"] is False


def test_saved_state_apply_updates_single_part_with_backup(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    original = b"old-payload"
    replacement = b"new-payload"
    state = {"data": original, "committed": False, "rolled_back": False, "sql": []}

    class FakeCursor:
        rowcount = 0

        def __init__(self, store: dict[str, Any]) -> None:
            self.store = store

        def execute(self, sql: str, params: tuple[Any, ...]) -> None:
            self.store["sql"].append(sql)
            if sql.strip().upper().startswith("SET LOCK_TIMEOUT"):
                return
            if sql.strip().upper().startswith("SELECT"):
                self.rowcount = 1
                return
            if sql.strip().upper().startswith("UPDATE"):
                self.store["data"] = params[0]
                self.rowcount = 1
                return
            raise AssertionError(sql)

        def fetchall(self) -> list[dict[str, Any]]:
            return [{"PartNo": 0, "BinaryData": self.store["data"]}]

    class FakeConn:
        def cursor(self, as_dict: bool = False) -> FakeCursor:
            return FakeCursor(state)

        def commit(self) -> None:
            state["committed"] = True

        def rollback(self) -> None:
            state["rolled_back"] = True

        def close(self) -> None:
            pass

    monkeypatch.setenv("ONEC_ADAPTER_BACKUP_DIR", str(tmp_path))
    monkeypatch.setattr(
        adapter_server,
        "connect_live_sql",
        lambda *args, **kwargs: (FakeConn(), {"server": "sql-host", "database": "upo_test"}, None),
    )
    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda *args, **kwargs: (state["data"], {"database": "upo_test"}, None),
    )
    monkeypatch.setattr(
        adapter_server,
        "invalidate_adapter_caches_after_saved_state_change",
        lambda base_id, *, reason, **kwargs: {"status": "ok", "base_id": base_id, "reason": reason},
    )

    proposal = {
        "schema": "onec_change_proposal.v1",
        "status": "accepted_for_review",
        "source": {"table": "ConfigSave", "file_name": "form-guid.0"},
        "original": {"sha1": adapter_server.hashlib.sha1(original).hexdigest(), "bytes": len(original)},
        "encoded": {
            "sha1": adapter_server.hashlib.sha1(replacement).hexdigest(),
            "bytes": len(replacement),
            "payload_hex": replacement.hex(),
        },
        "edits": [{"path": "1.2.3", "old": "old", "new": "new"}],
    }

    result = adapter_server.storage_saved_state_apply_proposal(
        {"base_id": "upo_test", "allow_sql_saved_state_apply": True, "proposal": proposal}
    )

    assert result["status"] == "applied"
    assert result["applied"] is True
    assert state["data"] == replacement
    assert state["committed"] is True
    assert state["rolled_back"] is False
    assert any("WITH (UPDLOCK, HOLDLOCK)" in sql for sql in state["sql"])
    assert result["cache_invalidation"]["reason"] == "saved_state_payload_apply"
    assert Path(result["backup"]["path"]).exists()
    backup = json.loads(Path(result["backup"]["path"]).read_text(encoding="utf-8"))
    assert backup["original"]["payload_hex"] == original.hex()


def test_saved_state_cache_invalidation_clears_persistent_and_runtime_views(monkeypatch: pytest.MonkeyPatch) -> None:
    base_id = "upo_test"
    other_base_id = "other"
    data_key = adapter_server.data_schema_cache_key(
        {"base_id": base_id, "ref": "Catalog.Номенклатура"}
    )
    other_data_key = adapter_server.data_schema_cache_key(
        {"base_id": other_base_id, "ref": "Catalog.Номенклатура"}
    )
    monkeypatch.setattr(
        adapter_server,
        "metadata_cache_invalidate",
        lambda payload: {
            "status": "ok",
            "counts": {"deleted": 7},
            "base_id": payload["base_id"],
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "BASE_ROOT_METADATA_CACHE",
        {
            (base_id, "Config"): {"cached_at": 1},
            (other_base_id, "Config"): {"cached_at": 1},
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "DATA_SCHEMA_CACHE",
        {
            data_key: {"cached_at": 1},
            other_data_key: {"cached_at": 1},
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "EXTENSION_MANIFEST_CACHE",
        {
            (base_id, "root-a"): {"status": "ok"},
            (other_base_id, "root-b"): {"status": "ok"},
        },
    )

    result = adapter_server.invalidate_adapter_caches_after_saved_state_change(
        base_id,
        reason="test",
    )

    assert result["status"] == "ok"
    assert result["persistent"]["deleted"] == 7
    assert result["runtime"] == {
        "base_root_metadata": 1,
        "data_schema": 1,
        "extension_manifests": 1,
    }
    assert list(adapter_server.BASE_ROOT_METADATA_CACHE) == [(other_base_id, "Config")]
    assert list(adapter_server.DATA_SCHEMA_CACHE) == [other_data_key]
    assert list(adapter_server.EXTENSION_MANIFEST_CACHE) == [(other_base_id, "root-b")]


def test_saved_state_apply_runs_form_element_semantic_verification(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    original = b"old-payload"
    replacement = b"new-payload"
    state = {"data": original}

    class FakeCursor:
        rowcount = 0

        def execute(self, sql: str, params: tuple[Any, ...]) -> None:
            if sql.strip().upper().startswith("UPDATE"):
                state["data"] = params[0]
                self.rowcount = 1

        def fetchall(self) -> list[dict[str, Any]]:
            return [{"PartNo": 0, "BinaryData": state["data"]}]

    class FakeConn:
        def cursor(self, as_dict: bool = False) -> FakeCursor:
            return FakeCursor()

        def commit(self) -> None:
            pass

        def rollback(self) -> None:
            pass

        def close(self) -> None:
            pass

    monkeypatch.setenv("ONEC_ADAPTER_BACKUP_DIR", str(tmp_path))
    monkeypatch.setattr(adapter_server, "connect_live_sql", lambda *args, **kwargs: (FakeConn(), {"server": "sql-host", "database": "upo_test"}, None))
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (state["data"], {"database": "upo_test"}, None))
    monkeypatch.setattr(
        adapter_server,
        "metadata_form_decode",
        lambda payload: {
            "status": "ok",
            "source": {"table": payload["table"], "file_name": payload["file_name"]},
            "profile": {"items": [{"name": "Кнопка", "id": "67", "title": "Новый заголовок", "path": "1.2"}]},
        },
    )
    proposal = {
        "schema": "onec_change_proposal.v1",
        "method": "metadata.form.element.write",
        "status": "accepted_for_review",
        "source": {"table": "ConfigSave", "file_name": "form-guid.0"},
        "original": {"sha1": adapter_server.hashlib.sha1(original).hexdigest(), "bytes": len(original)},
        "encoded": {"sha1": adapter_server.hashlib.sha1(replacement).hexdigest(), "bytes": len(replacement), "payload_hex": replacement.hex()},
        "element": {"id": "67", "name": "Кнопка"},
        "form_element_edits": [{"property": "title", "value": "Новый заголовок"}],
    }

    result = adapter_server.storage_saved_state_apply_proposal(
        {"base_id": "upo_test", "allow_sql_saved_state_apply": True, "proposal": proposal}
    )

    assert result["status"] == "applied"
    assert result["semantic_verification"]["status"] == "ok"
    assert result["semantic_verification"]["checks"] == [
        {"property": "title", "expected": "Новый заголовок", "actual": "Новый заголовок", "ok": True}
    ]


def test_saved_state_apply_runs_form_command_semantic_verification(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    original = b"old-payload"
    replacement = b"new-payload"
    state = {"data": original}

    class FakeCursor:
        rowcount = 0

        def execute(self, sql: str, params: tuple[Any, ...]) -> None:
            if sql.strip().upper().startswith("UPDATE"):
                state["data"] = params[0]
                self.rowcount = 1

        def fetchall(self) -> list[dict[str, Any]]:
            return [{"PartNo": 0, "BinaryData": state["data"]}]

    class FakeConn:
        def cursor(self, as_dict: bool = False) -> FakeCursor:
            return FakeCursor()

        def commit(self) -> None:
            pass

        def rollback(self) -> None:
            pass

        def close(self) -> None:
            pass

    monkeypatch.setenv("ONEC_ADAPTER_BACKUP_DIR", str(tmp_path))
    monkeypatch.setattr(adapter_server, "connect_live_sql", lambda *args, **kwargs: (FakeConn(), {"server": "sql-host", "database": "upo_test"}, None))
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (state["data"], {"database": "upo_test"}, None))
    monkeypatch.setattr(
        adapter_server,
        "metadata_form_decode",
        lambda payload: {
            "status": "ok",
            "source": {"table": payload["table"], "file_name": payload["file_name"]},
            "profile": {"items": [], "commands": [{"name": "КомандаПример1", "id": "2", "title": "ПРОВЕРКА", "path": "5.3"}]},
        },
    )
    proposal = {
        "schema": "onec_change_proposal.v1",
        "method": "metadata.form.element.write",
        "status": "accepted_for_review",
        "source": {"table": "ConfigCASSave", "file_name": "form-guid.0"},
        "original": {"sha1": adapter_server.hashlib.sha1(original).hexdigest(), "bytes": len(original)},
        "encoded": {"sha1": adapter_server.hashlib.sha1(replacement).hexdigest(), "bytes": len(replacement), "payload_hex": replacement.hex()},
        "element": {"section": "commands", "path": "5.3", "name": "КомандаПример1"},
        "form_element_edits": [{"property": "title", "value": "ПРОВЕРКА"}],
    }

    result = adapter_server.storage_saved_state_apply_proposal(
        {"base_id": "upo_test", "allow_sql_saved_state_apply": True, "proposal": proposal}
    )

    assert result["status"] == "applied"
    assert result["semantic_verification"]["status"] == "ok"
    assert result["semantic_verification"]["element"]["section"] == "commands"


def test_saved_state_apply_runs_form_attribute_field_semantic_verification(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    original = b"old-payload"
    replacement = b"new-payload"
    state = {"data": original}

    class FakeCursor:
        rowcount = 0

        def execute(self, sql: str, params: tuple[Any, ...]) -> None:
            if sql.strip().upper().startswith("UPDATE"):
                state["data"] = params[0]
                self.rowcount = 1

        def fetchall(self) -> list[dict[str, Any]]:
            return [{"PartNo": 0, "BinaryData": state["data"]}]

    class FakeConn:
        def cursor(self, as_dict: bool = False) -> FakeCursor:
            return FakeCursor()

        def commit(self) -> None:
            pass

        def rollback(self) -> None:
            pass

        def close(self) -> None:
            pass

    monkeypatch.setenv("ONEC_ADAPTER_BACKUP_DIR", str(tmp_path))
    monkeypatch.setattr(adapter_server, "connect_live_sql", lambda *args, **kwargs: (FakeConn(), {"server": "sql-host", "database": "upo_test"}, None))
    monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (state["data"], {"database": "upo_test"}, None))
    monkeypatch.setattr(
        adapter_server,
        "metadata_form_decode",
        lambda payload: {
            "status": "ok",
            "source": {"table": payload["table"], "file_name": payload["file_name"]},
            "profile": {
                "items": [],
                "attributes": [
                    {
                        "name": "ТЗ",
                        "id": "7",
                        "path": "3.6",
                        "dynamic_list_fields": [
                            {"name": "К1", "id": "1", "title": "ТЕСТ_К1", "path": "3.6.14", "title_path": "3.6.14.4.2.1"}
                        ],
                    }
                ],
            },
        },
    )
    proposal = {
        "schema": "onec_change_proposal.v1",
        "method": "metadata.form.element.write",
        "status": "accepted_for_review",
        "source": {"table": "ConfigCASSave", "file_name": "form-guid.0"},
        "original": {"sha1": adapter_server.hashlib.sha1(original).hexdigest(), "bytes": len(original)},
        "encoded": {"sha1": adapter_server.hashlib.sha1(replacement).hexdigest(), "bytes": len(replacement), "payload_hex": replacement.hex()},
        "element": {"section": "attribute_fields", "path": "3.6.14", "name": "К1"},
        "form_element_edits": [{"property": "title", "value": "ТЕСТ_К1"}],
    }

    result = adapter_server.storage_saved_state_apply_proposal(
        {"base_id": "upo_test", "allow_sql_saved_state_apply": True, "proposal": proposal}
    )

    assert result["status"] == "applied"
    assert result["semantic_verification"]["status"] == "ok"
    assert result["semantic_verification"]["element"]["section"] == "attribute_fields"


def test_saved_state_rollback_loads_backup_by_id(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    backup_id = "a" * 32
    backup_path = tmp_path / f"20260625T010000Z-{backup_id}.json"
    backup_path.write_text(
        json.dumps(
            {
                "schema": "onec_storage_apply_backup.v1",
                "backup_id": backup_id,
                "source": {"table": "ConfigSave", "file_name": "form-guid.0"},
                "original": {"sha1": "old", "payload_hex": "00"},
                "replacement": {"sha1": "new"},
                "rollback": {
                    "request": {
                        "base_id": "upo_test",
                        "allow_sql_saved_state_apply": True,
                        "proposal": {
                            "source": {"table": "ConfigSave", "file_name": "form-guid.0"},
                            "original": {"sha1": "new"},
                            "encoded": {"sha1": "old", "payload_hex": "00"},
                        },
                    }
                },
            },
            ensure_ascii=False,
        ),
        encoding="utf-8",
    )
    seen: dict[str, Any] = {}
    monkeypatch.setenv("ONEC_ADAPTER_BACKUP_DIR", str(tmp_path))

    def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
        seen["payload"] = payload
        return {"status": "applied", "applied": True, "readback": {"verified": True}}

    monkeypatch.setattr(adapter_server, "storage_saved_state_apply_proposal", fake_apply)

    result = adapter_server.storage_saved_state_rollback(
        {"base_id": "upo_test", "allow_sql_saved_state_rollback": True, "backup_id": backup_id}
    )

    assert result["status"] == "applied"
    assert result["applied"] is True
    assert result["backup"]["backup_id"] == backup_id
    assert seen["payload"]["allow_sql_saved_state_apply"] is True
    assert seen["payload"]["base_id"] == "upo_test"


def test_saved_state_rollback_preserves_public_support_selector(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    backup_id = "a" * 32
    backup_path = tmp_path / f"backup-{backup_id}.json"
    backup_path.write_text(json.dumps({"backup_id": backup_id, "rollback": {"payload": {"proposal": {}}}}), encoding="utf-8")
    monkeypatch.setenv("ONEC_ADAPTER_BACKUP_DIR", str(tmp_path))
    seen: dict[str, Any] = {}
    monkeypatch.setattr(adapter_server, "storage_saved_state_apply_proposal", lambda payload: seen.setdefault("payload", payload) or {"status": "applied", "applied": True})
    adapter_server.storage_saved_state_rollback({"base_id": "upo_test", "backup_id": backup_id, "allow_sql_saved_state_rollback": True, "ref": "Report.Тест", "kind": "Report", "name": "Тест"})
    assert seen["payload"]["ref"] == "Report.Тест"
    assert seen["payload"]["kind"] == "Report"


def test_swap_brace_text_paths_preserves_surrounding_format() -> None:
    from parser.payload import parse_brace_text, scalar, swap_brace_text_paths, get_tree_path

    text = '{0, {48,"А",{1,2}}, {99}, {48,"Б",{3,4}}}'

    swapped, info = swap_brace_text_paths(text, "1", "3")
    tree = parse_brace_text(swapped)

    assert info["path_a"] == "1"
    assert info["path_b"] == "3"
    assert swapped == '{0, {48,"Б",{3,4}}, {99}, {48,"А",{1,2}}}'
    assert scalar(get_tree_path(tree, "1.1")) == "Б"
    assert scalar(get_tree_path(tree, "3.1")) == "А"


def test_changes_propose_supports_preserve_format_structural_swap(monkeypatch: pytest.MonkeyPatch) -> None:
    text = '{0, {48,"А",{1,2}}, {99}, {48,"Б",{3,4}}}'
    original = text.encode("utf-8")

    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda *args, **kwargs: (original, {"database": "upo_test"}, None),
    )

    result = adapter_server.changes_propose(
        {
            "base_id": "upo_test",
            "source": {"table": "ConfigCASSave", "file_name": "form.0"},
            "edits": [{"swap_paths": ["1", "3"]}],
            "preserve_format": True,
            "include_payload": True,
        }
    )

    assert result["status"] == "accepted_for_review"
    assert result["validation"]["status"] == "ok"
    assert result["validation"]["mode"] == "path_preserve_format"
    assert result["edits"][0]["mode"] == "structural_swap_preserve_format"
    assert result["encoded"]["bytes"] == result["original"]["bytes"]
    assert bytes.fromhex(result["encoded"]["payload_hex"]).decode("utf-8") == '{0, {48,"Б",{3,4}}, {99}, {48,"А",{1,2}}}'


def test_changes_propose_supports_verified_root_replacement(monkeypatch: pytest.MonkeyPatch) -> None:
    from parser.payload import parse_brace_text

    original = b"{1,2,3}"
    replacement = parse_brace_text("{1,2,3,4,5}")
    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda *args, **kwargs: (original, {"database": "upo_test"}, None),
    )

    result = adapter_server.changes_propose(
        {
            "base_id": "upo_test",
            "source": {"table": "ConfigSave", "file_name": "scheduled-job.0"},
            "edits": [
                {
                    "replace_root": replacement,
                    "fields": ["week_days"],
                    "resized_collections": ["week_days"],
                    "old_counts": {"week_days": 1},
                    "new_counts": {"week_days": 3},
                }
            ],
            "preserve_format": True,
            "include_payload": True,
        }
    )

    assert result["status"] == "accepted_for_review"
    assert result["validation"]["status"] == "ok"
    assert result["edits"][0]["mode"] == "structural_replace_root_preserve_prefix"
    assert result["edits"][0]["old_counts"] == {"week_days": 1}
    assert result["edits"][0]["new_counts"] == {"week_days": 3}
    assert bytes.fromhex(result["encoded"]["payload_hex"]).decode("utf-8-sig") == "{1,2,3,4,5}"


def test_changes_propose_repairs_mojibake_stream_before_replace(monkeypatch: pytest.MonkeyPatch) -> None:
    from parser.cas_payload import stream_header

    good_text = "Процедура ОбработкаКоманды()\r\n\t// Старый\r\nКонецПроцедуры\r\n"
    mojibake_text = good_text.encode("utf-8").decode("cp1251")
    stream_bytes = mojibake_text.encode("utf-8")
    stored = compress_payload(stream_header(len(stream_bytes)) + stream_bytes, "raw_deflate")

    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda *args, **kwargs: (stored, {"database": "upo_test"}, None),
    )

    result = adapter_server.changes_propose(
        {
            "base_id": "upo_test",
            "source": {"table": "ConfigCASSave", "file_name": "object__module.0", "module_id": "ConfigCASSave:object__module.0#stream:0"},
            "edits": [
                {
                    "replace": {"old": "// Старый", "new": "// Новый"},
                    "expected_contains": "Процедура ОбработкаКоманды",
                }
            ],
            "include_payload": True,
        }
    )

    assert result["status"] == "accepted_for_review"
    assert result["edits"][0]["encoding_repaired"] is True
    assert "Процедура ОбработкаКоманды" in result["edits"][0]["old_text_preview"]
    assert "// Новый" in result["edits"][0]["new_text_preview"]
    assert not result["edits"][0]["new_text_preview"].startswith("\ufeff")


def test_changes_propose_resizes_complete_declared_bsl_stream(monkeypatch: pytest.MonkeyPatch) -> None:
    """A declared BSL stream without an opaque tail is safely resizable."""
    from parser.cas_payload import stream_header, structural_stream_blocks_with_data

    old = "// Старый\r\n"
    new = "// Новый комментарий переменной длины\r\n"
    source = ("\ufeff" + old).encode("utf-8")
    stored = compress_payload(stream_header(len(source)) + source, "raw_deflate")
    monkeypatch.setattr(
        adapter_server,
        "read_storage_file_bytes",
        lambda *_args, **_kwargs: (stored, {"database": "upo_test"}, None),
    )

    result = adapter_server.changes_propose(
        {
            "base_id": "upo_test",
            "source": {"table": "ConfigCASSave", "file_name": "object__module.2", "module_id": "ConfigCASSave:object__module.2#stream:0"},
            "edits": [{"stream_index": 0, "replace": {"old": old, "new": new, "count": 1}}],
            "include_payload": True,
        }
    )

    assert result["status"] == "accepted_for_review"
    assert result["edits"][0]["mode"] == "stream"
    assert result["edits"][0]["old_bytes"] != result["edits"][0]["new_bytes"]
    decoded = decode_payload_lossless(bytes.fromhex(result["encoded"]["payload_hex"]))
    block = structural_stream_blocks_with_data(bytes(decoded["payload"]))[0]
    assert block["declared_1"] == block["declared_2"] == result["edits"][0]["new_bytes"]
    assert new in str(block["text"])


@pytest.mark.parametrize(
    ("annotation", "expected"),
    [
        ("Перед", "insert_before"),
        ("После", "insert_after"),
        ("Вместо", "replace"),
        ("ИзменениеИКонтроль", "replace_with_control"),
    ],
)
def test_extension_action_is_decoded_from_bsl_annotation(annotation: str, expected: str) -> None:
    result = adapter_server.metadata_extension_action_from_evidence(
        source="extension",
        method_name="ПередЗаписью",
        read_result={"text": f'&{annotation}("ПередЗаписью")\nПроцедура РасширениеПередЗаписью()\nКонецПроцедуры'},
    )

    assert result["status"] == "ok"
    assert result["operation_class"] == expected


def test_effective_extension_routine_scan_orders_and_proves_form_candidates(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "extension_map_by_guid",
        lambda _base_id: {
            "late": {"name": "Late", "guid": "late", "order": 2, "active": True},
            "early": {"name": "Early", "guid": "early", "order": 1, "active": True},
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "search_modules",
        lambda payload: {
            "status": "ok",
            "matches": [{"read_selector": {"module_ref": "ConfigCASSave:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa__f5f217c8-ac69-4e23-966b-d8ec0c665638.0"}}]
            if payload["extension"] == "early"
            else [],
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "repository_resolve_form_guid_sql",
        lambda **_kwargs: ({"resolved_object": "Document.Тест.Form.Форма"}, None),
    )
    monkeypatch.setattr(
        adapter_server,
        "read_module",
        lambda _payload: {"status": "ok", "text": '&После("Метод")\nПроцедура РасшМетод()\nКонецПроцедуры'},
    )

    layers = adapter_server.effective_extension_routine_scan("upo_test", "Метод", canonical_object="Document.Тест.Form.Форма")

    assert [layer["extension"]["name"] for layer in layers] == ["Early", "Late"]
    assert layers[0]["applicability"] == "proven"
    assert layers[0]["extension_actions"][0]["operation_class"] == "insert_after"
    assert layers[1]["applicability"] == "not_found"


def test_metadata_resolve_overrides_marks_unknown_extension_action(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_modules",
        lambda payload: {
            "status": "ok",
            "object": {"kind": "Catalog", "name": "Номенклатура"},
            "modules": [{"module_id": "ConfigCAS:ext-guid__module-guid.0", "name": "object"}],
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "read_module",
        lambda payload: {
            "status": "ok",
            "selection": {"routine_name": "ПередЗаписью", "line_start": 1, "line_end": 3, "match_by": "routine_exact"},
        },
    )

    result = adapter_server.metadata_resolve_overrides(
        {
            "base_id": "upo_test",
            "object_type": "Catalog",
            "object_name": "Номенклатура",
            "method_name": "ПередЗаписью",
        }
    )

    assert result["status"] == "ok"
    action = result["chain"][0]["extension_action"]
    assert action["status"] == "unknown"
    assert action["operation_class"] == "unknown_extension_action"
    assert "replace_with_control" in action["requires"][0]
    assert result["extension_actions"] == [action]
    evidence = result["write_plan_evidence"]
    assert evidence["method"] == "metadata.write.plan"
    assert evidence["target"]["kind"] == "module"
    assert evidence["target"]["routine_name"] == "ПередЗаписью"
    assert evidence["target"]["extension_action"] == action
    assert evidence["next_resolution"]["method"] == adapter_server.SAVED_STATE_MODULES_SEARCH_METHOD
    assert evidence["next_resolution"]["params"]["query"] == "ПередЗаписью"
    assert evidence["next_resolution"]["params"]["layer"] == "extension_saved_state"
    assert evidence["next_resolution"]["params"]["ref"] == "Catalog.Номенклатура"
    assert "tables" not in evidence["next_resolution"]["params"]
    assert "owner_guid" not in evidence["next_resolution"]["params"]
    assert "intent" not in evidence


def test_metadata_resolve_overrides_uses_saved_extension_form_by_public_object(monkeypatch: pytest.MonkeyPatch) -> None:
    active_ref = "ConfigCAS:" + ("a" * 40) + "#form_module"
    saved_ref = "ConfigCASSave:fb26cf42-7609-11f1-828f-005056b0d483__77494708-43ea-4956-ac3c-199cfb035ad2.0#form_module"
    read_refs: list[str] = []
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_modules",
        lambda payload: {
            "status": "ok",
            "object": {
                "kind": "Catalog",
                "name": "test2",
                "guid": "e7e322f5-a7a6-4ddc-bad0-344e75ad00d0",
                "storage": {"table": "ConfigCAS", "file_name": "hidden"},
                "origin": {"extension": {"name": "test2", "guid": "fb26cf42-7609-11f1-828f-005056b0d483"}},
            },
            "modules": [
                {
                    "module_id": active_ref,
                    "kind": "form_module",
                    "form": {"name": "t_Форма", "guid": "77494708-43ea-4956-ac3c-199cfb035ad2"},
                }
            ],
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "code_read_saved_counterpart_module_ref",
        lambda base_id, module_ref, payload: saved_ref,
    )

    def fake_read(payload: dict[str, Any]) -> dict[str, Any]:
        read_refs.append(str(payload.get("module_ref") or ""))
        return {
            "status": "ok",
            "text": '&После("ЗаменаДомена")\nПроцедура РасширениеЗаменаДомена()\nКонецПроцедуры',
            "selection": {
                "routine_name": "ЗаменаДомена",
                "line_start": 1,
                "line_end": 3,
                "match_by": "routine_exact",
                "operation_class": "insert_after",
            },
        }

    monkeypatch.setattr(adapter_server, "read_module", fake_read)

    result = adapter_server.metadata_resolve_overrides(
        {
            "base_id": "upo_test",
            "ref": "Catalog.test2",
            "method_name": "ЗаменаДомена",
            "state": "working",
        }
    )

    assert result["status"] == "ok"
    assert read_refs == [saved_ref]
    assert result["chain"][0]["source"] == "extension"
    assert result["chain"][0]["activation_state"] == "saved_state"
    assert result["chain"][0]["module"]["form"] == "t_Форма"
    assert result["chain"][0]["read_selector"]["module_ref"] == saved_ref
    assert result["chain"][0]["extension_action"]["operation_class"] == "insert_after"
    assert "storage" not in result["object"]


def test_metadata_resolve_overrides_classifies_plain_extension_owned_routine_as_definition(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_modules",
        lambda payload: {
            "status": "ok",
            "object": {
                "kind": "Catalog",
                "name": "test2",
                "guid": "e7e322f5-a7a6-4ddc-bad0-344e75ad00d0",
                "origin": {"source": "extension", "extension": {"name": "test2"}},
            },
            "modules": [{"module_id": "ConfigCAS:" + ("a" * 40) + "#form_module", "kind": "form_module"}],
        },
    )
    monkeypatch.setattr(adapter_server, "code_read_saved_counterpart_module_ref", lambda *args, **kwargs: None)
    monkeypatch.setattr(
        adapter_server,
        "read_module",
        lambda payload: {
            "status": "ok",
            "text": "Процедура ЗаменаДомена()\nКонецПроцедуры",
            "selection": {"line_start": 1, "line_end": 2, "match_by": "routine_exact"},
        },
    )

    result = adapter_server.metadata_resolve_overrides(
        {
            "base_id": "upo_test",
            "ref": "Catalog.test2",
            "method_name": "ЗаменаДомена",
            "state": "working",
        }
    )

    action = result["chain"][0]["extension_action"]
    assert action["status"] == "ok"
    assert action["operation_class"] == "extension_definition"
    assert result["write_plan_evidence"]["target"]["extension_action"] == action
    assert "intent" not in result["write_plan_evidence"]
    assert adapter_server.metadata_write_plan_extension_action_problem(action, "replace_with_control", False) is None


def test_metadata_resolve_overrides_preserves_replace_with_control_action(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_modules",
        lambda payload: {
            "status": "ok",
            "object": {"kind": "Catalog", "name": "Номенклатура"},
            "modules": [{"module_id": "ConfigCAS:ext-guid__module-guid.0", "name": "object"}],
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "read_module",
        lambda payload: {
            "status": "ok",
            "selection": {
                "routine_name": "ПередЗаписью",
                "line_start": 1,
                "line_end": 3,
                "match_by": "routine_exact",
                "operation_class": "replace_with_control",
            },
        },
    )

    result = adapter_server.metadata_resolve_overrides(
        {
            "base_id": "upo_test",
            "object_type": "Catalog",
            "object_name": "Номенклатура",
            "method_name": "ПередЗаписью",
        }
    )

    action = result["chain"][0]["extension_action"]
    assert action["status"] == "ok"
    assert action["operation_class"] == "replace_with_control"
    assert action["requires_control_fragment"] is True
    evidence = result["write_plan_evidence"]
    assert evidence["target"]["extension_action"] == action
    assert evidence["intent"]["operation"] == "replace_with_control"
    assert evidence["next_resolution"]["method"] == adapter_server.SAVED_STATE_MODULES_SEARCH_METHOD
    assert evidence["next_resolution"]["params"]["layer"] == "extension_saved_state"
    assert evidence["next_resolution"]["params"]["ref"] == "Catalog.Номенклатура"


def test_metadata_resolve_overrides_base_evidence_points_to_configsave(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(
        adapter_server,
        "metadata_object_modules",
        lambda payload: {
            "status": "ok",
            "object": {"kind": "Catalog", "name": "Номенклатура", "guid": "owner-guid"},
            "modules": [{"module_id": "Config:owner-guid__module-guid.0", "name": "object"}],
        },
    )
    monkeypatch.setattr(
        adapter_server,
        "read_module",
        lambda payload: {
            "status": "ok",
            "selection": {"routine_name": "ПередЗаписью", "line_start": 1, "line_end": 3, "match_by": "routine_exact"},
        },
    )

    result = adapter_server.metadata_resolve_overrides(
        {
            "base_id": "upo_test",
            "object_type": "Catalog",
            "object_name": "Номенклатура",
            "method_name": "ПередЗаписью",
        }
    )

    evidence = result["write_plan_evidence"]
    assert evidence["next_resolution"]["method"] == adapter_server.SAVED_STATE_MODULES_SEARCH_METHOD
    assert evidence["next_resolution"]["params"]["layer"] == "base_saved_state"
    assert evidence["next_resolution"]["params"]["ref"] == "Catalog.Номенклатура"
    assert "tables" not in evidence["next_resolution"]["params"]
    assert "owner_guid" not in evidence["next_resolution"]["params"]
    assert evidence["target"]["object_guid"] == "owner-guid"


def test_form_long_command_bar_decodes_tail_autofill() -> None:
    from parser.form_payload import enrich_container_specific_semantics

    values = ["0"] * 83
    values[0] = "22"
    values[82] = "0"
    row = {"marker": "22", "type_name": "Командная панель", "semantic": {"groups": {}}}
    enrich_container_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))
    props = {p["name"]: p["value"] for p in row["semantic"]["groups"]["Использование"]}
    assert props["Автозаполнение"] is False


def test_form_managed_group_decodes_spacing_and_horizontal_alignment() -> None:
    from parser.form_payload import enrich_container_specific_semantics

    managed = "{29,1,0,0,0,{0},{1,0},{\"Pattern\"},\"\",{3,4,{0}},0,0,0,1,{1,0},0,2,1,3,2,0,1,1,{3,4,{0}},0,2,0,3,0}"
    values = ["0"] * 30
    values[0] = "22"
    values[20] = managed
    row = {"marker": "22", "type_name": "Группа", "semantic": {"groups": {}}}
    enrich_container_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))
    props = {p["name"]: p["value"] for p in row["semantic"]["groups"]["Расположение"]}
    assert props["VerticalSpacing"] == "Half"
    assert props["ГоризонтальноеПоложениеВГруппе"] == "Center"


def test_form_decoration_decodes_balloon_tooltip_and_windows_text_color() -> None:
    from parser.form_payload import enrich_decoration_specific_semantics

    values = ["0"] * 36
    values[0] = "12"
    values[14] = "{3,1,{18}}"
    values[18] = "{4,0,0,0,0,0,{3,4,{0}}}"
    values[22] = "2"
    row = {"marker": "12", "semantic": {"groups": {}}}
    enrich_decoration_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))
    props = {p["name"]: p["value"] for p in row["semantic"]["groups"]["Оформление"]}
    assert props["ЦветТекста"] == "win:ButtonText"
    assert props["ОтображениеПодсказки"] == "Balloon"


def test_form_command_record_decodes_representation_and_shortcut() -> None:
    from parser.form_payload import direct_parameters, section_record_semantic_properties

    node = parse_brace_text('{9,{51,409b9a53-7f7e-4178-86c1-33176c7c7a7a},"ОтправитьQR",{1,0},{1,0},{0,{0,{"B",1},0}},{0,83,8},{1,0},"ОтправитьQR",2,0,0,{0,0},1,0,1,0,0,2}')
    row = {"category": "Command", "name": "ОтправитьQR"}
    semantic = section_record_semantic_properties(row, direct_parameters(node, "5.47"), node)
    props = {p["name"]: p["value"] for group in semantic["groups"].values() for p in group}
    assert props["Отображение"] == "TextPicture"
    assert props["СочетаниеКлавиш"] == "Ctrl+S"
    assert props["CurrentRowUse"] == "DontUse"


def test_form_command_record_decodes_function_key_without_modifiers() -> None:
    from parser.form_payload import direct_parameters, section_record_semantic_properties

    node = parse_brace_text('{9,0,"Обновить",0,0,0,{0,116,0},0,"Обновить",3,0,0,0,1,0,1,0,0,1}')
    row = {"category": "Command", "name": "Обновить"}
    semantic = section_record_semantic_properties(row, direct_parameters(node, "5.4"), node)
    props = {p["name"]: p["value"] for group in semantic["groups"].values() for p in group}

    assert props["СочетаниеКлавиш"] == "F5"


def test_form_command_record_decodes_alt_modifier_and_picture_representation() -> None:
    from parser.form_payload import direct_parameters, section_record_semantic_properties

    node = parse_brace_text('{9,0,"Копировать",0,0,0,{0,67,24},0,"Копировать",1,0,0,0,1,0,1,0,0,1}')
    row = {"category": "Command", "name": "Копировать"}
    semantic = section_record_semantic_properties(row, direct_parameters(node, "5.8"), node)
    props = {p["name"]: p["value"] for group in semantic["groups"].values() for p in group}

    assert props["СочетаниеКлавиш"] == "Ctrl+Alt+C"
    assert props["Отображение"] == "Picture"
    assert props["CurrentRowUse"] == "DontUse"


def test_form_input_choice_button_representation_show_in_input_field() -> None:
    from parser.form_payload import choice_button_representation_presentation

    assert choice_button_representation_presentation("3") == "ShowInInputField"


def test_form_standard_document_commands_are_known() -> None:
    from parser.form_payload import FORM_STANDARD_COMMANDS

    assert FORM_STANDARD_COMMANDS[("fe558fde-99b3-45d0-a060-9fc2905309f6", "0")] == "Form.StandardCommand.Write"
    assert FORM_STANDARD_COMMANDS[("174e58ce-82ad-4787-b956-9367937f7971", "0")] == "Form.StandardCommand.ChangeHistory"


def test_graphical_schema_standard_command_resolves_public_owner_path() -> None:
    from parser.form_payload import button_command_links

    items = [
        {"marker": "37", "type_name": "GraphicalSchemaField", "name": "КартаМаршрута"},
        {
            "marker": "31",
            "name": "ФормаПечать",
            "command_reference": {"guid": "e2d6f793-b786-4640-a91b-8d77f73860f1", "code": "3"},
        },
    ]

    links = button_command_links(items, [])

    assert links[0]["command_name"] == "Form.Item.КартаМаршрута.StandardCommand.Print"


def test_dynamic_list_standard_and_task_commands_resolve_public_names() -> None:
    from parser.form_payload import button_command_links

    items = [
        {
            "marker": "55",
            "type_name": "Динамический список",
            "name": "Список",
            "id": "1",
            "dynamic_list_settings": {"query_text": "ВЫБРАТЬ Ссылка ИЗ Задача.ЗадачаИсполнителя"},
        },
        {
            "marker": "31",
            "name": "Изменить",
            "command_reference": {"guid": "b41f5bbc-ba5d-4888-8cd1-db246a371418", "code": "1"},
        },
        {
            "marker": "31",
            "name": "Выполнить",
            "command_reference": {"guid": "0fa77ef7-a836-4459-9bca-6010d0bdfc7f", "code": "0"},
        },
    ]

    links = button_command_links(items, [])

    assert links[0]["command_name"] == "Form.Item.Список.StandardCommand.Change"
    assert links[1]["command_name"] == "Task.ЗадачаИсполнителя.Command.Выполнено"


def test_form_parameter_rows_decode_key_parameter() -> None:
    from parser.form_payload import form_parameter_rows

    tree = parse_brace_text('{4,0,0,0,{0,1,{0,"БизнесПроцесс",{0},1}}}')

    rows = form_parameter_rows(tree)

    assert rows[0]["name"] == "БизнесПроцесс"
    assert rows[0]["type_name"] == "Parameter"
    props = {prop["name"]: prop["value"] for group in rows[0]["semantic"]["groups"].values() for prop in group}
    assert props["KeyParameter"] is True


def test_form_legacy_special_field_and_english_tooltip_names_are_public() -> None:
    from parser.form_payload import form_item_public_type_name

    assert form_item_public_type_name("37", "11", "Диаграмма") == "ChartField"
    assert form_item_public_type_name("37", "14", "КартаМаршрута") == "GraphicalSchemaField"
    assert form_item_public_type_name("12", "0", "КартаМаршрутаExtendedTooltip") == "Расширенная подсказка"


def test_picture_field_decodes_size_stretch_and_file_drag_mode() -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    values = ["0"] * 40
    values[0] = "37"
    values[5] = "4"
    specific = ["0"] * 24
    specific[0] = "10"
    specific[1] = "2"
    specific[2] = "0"
    specific[17] = "1"
    values[39] = "{" + ",".join(specific) + "}"
    row = {"marker": "37", "type_code": "4", "semantic": {"groups": {}}}

    enrich_input_field_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))

    props = {prop["name"]: prop["value"] for group in row["semantic"]["groups"].values() for prop in group}
    assert props["Ширина"] == "2"
    assert props["РастягиватьПоГоризонтали"] is False
    assert props["РежимПеретаскиванияФайлов"] == "AsFile"


def test_form_command_button_decodes_default_button() -> None:
    from parser.form_payload import enrich_command_button_specific_semantics

    values = ["0"] * 51
    values[0] = "31"
    values[11] = "1"
    row = {"marker": "31", "semantic": {"groups": {}}}
    enrich_command_button_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))
    props = {p["name"]: p["value"] for p in row["semantic"]["groups"]["Основные"]}
    assert props["КнопкаПоУмолчанию"] is True


def test_form_catalog_input_field_decodes_bounds_domain_footer_and_border() -> None:
    from parser.form_payload import enrich_input_field_specific_semantics

    values = ["0"] * 59
    values[0] = "37"
    values[5] = "2"
    values[6] = '"Наценка"'
    specific = ["0"] * 66
    specific[0] = "36"
    specific[16] = '{"N",-99}'
    specific[17] = '{"N",100}'
    specific[32] = "0"
    specific[39] = "{3,3,{-22}}"
    specific[45] = "2"
    values[39] = "{" + ",".join(specific) + "}"
    row = {"marker": "37", "type_code": "2", "name_path": "1.6", "semantic": {"groups": {}}}

    enrich_input_field_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))

    props = {p["name"]: p["value"] for group in row["semantic"]["groups"].values() for p in group}
    assert props["TypeDomainEnabled"] is False
    assert props["MinValue"] == "-99"
    assert props["MaxValue"] == "100"
    assert props["FooterHorizontalAlign"] == "Left"
    assert props["BorderColor"] == "style:BorderColor"


def test_form_radio_button_auto_type_and_columns_count() -> None:
    from parser.form_payload import enrich_radio_button_specific_semantics

    values = ["0"] * 59
    values[0] = "37"
    values[5] = "5"
    values[39] = "{8,{3,1},1,0,0,0,0,0,0,0,0,2}"
    row = {"marker": "37", "type_code": "5", "semantic": {"groups": {}}}
    enrich_radio_button_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))
    props = {p["name"]: p["value"] for group in row["semantic"]["groups"].values() for p in group}
    assert props["RadioButtonType"] == "Auto"
    assert props["ColumnsCount"] == "1"


def test_extension_filter_rejects_registered_inactive_extension(monkeypatch: pytest.MonkeyPatch) -> None:
    guid = "11111111-1111-1111-1111-111111111111"
    inactive = {guid: {"guid": guid, "name": "ВыключенноеРасширение", "active": False}}
    monkeypatch.setattr(adapter_server, "extension_map_by_guid", lambda _base_id: {})
    monkeypatch.setattr(adapter_server, "registered_extension_map_by_guid", lambda _base_id: inactive)

    resolved, error = adapter_server.extension_filter_to_guid("upo_test", guid, method="modules.search")

    assert resolved is None
    assert error is not None
    assert error["status"] == "unavailable"
    assert error["error"] == "extension_inactive"


def test_call_dispatch_rejects_inactive_extension_guid_before_route(monkeypatch: pytest.MonkeyPatch) -> None:
    guid = "11111111-1111-1111-1111-111111111111"
    monkeypatch.setattr(
        adapter_server,
        "extension_filter_to_guid",
        lambda _base_id, _selector, method: (None, {
            "schema": f"onec_{method.replace('.', '_')}.v1",
            "status": "unavailable",
            "error": "extension_inactive",
        }),
    )

    result = adapter_server.call_method_impl("modules.search", {"base_id": "upo_test", "extension_guid": guid, "query": "Probe"})

    assert result["status"] == "unavailable"
    assert result["error"] == "extension_inactive"


def test_inactive_extension_cassave_file_route_is_rejected(monkeypatch: pytest.MonkeyPatch) -> None:
    guid = "11111111-1111-1111-1111-111111111111"
    expected = {"status": "unavailable", "error": "extension_inactive"}
    monkeypatch.setattr(adapter_server, "reject_inactive_extension_scope", lambda *_args, **_kwargs: expected)

    result = adapter_server.reject_inactive_storage_file(
        "upo_test", "ConfigCASSave", f"{guid}__configinfo", method="storage.file.get"
    )

    assert result == expected








