from __future__ import annotations

from pathlib import Path

import pytest

from pydantic import ConfigDict

from sync_state_machine.config import BaseDataSourceConfig, build_config_from_file, config_snapshot, register_datasource_type
from sync_state_machine.config.run_presets import load_overrides_from_file
from sync_state_machine.datasource import BaseDataSource


def test_load_overrides_from_yaml_file(tmp_path: Path) -> None:
    pytest.importorskip("yaml")

    yaml_file = tmp_path / "profile.yaml"
    yaml_file.write_text(
        """
node_types:
  - company
logging:
  initialize: true
  suppress_node_logs: true
local_datasource:
  type: jsonl
  jsonl_dir: ${PROJECT_ROOT}/filtered_datasource/datasource/filtered
""".strip(),
        encoding="utf-8",
    )

    overrides = load_overrides_from_file(yaml_file, project_root=tmp_path)

    assert overrides["logging"]["suppress_node_logs"] is True
    assert str(tmp_path) in overrides["local_datasource"]["jsonl_dir"]


def test_load_overrides_ignores_extensions_bootstraps(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    bootstrap_module = tmp_path / "profile_bootstrap_demo.py"
    bootstrap_module.write_text(
        "BOOTSTRAP_CALLED = False\n"
        "\n"
        "def register():\n"
        "    global BOOTSTRAP_CALLED\n"
        "    BOOTSTRAP_CALLED = True\n",
        encoding="utf-8",
    )
    monkeypatch.syspath_prepend(str(tmp_path))

    yaml_file = tmp_path / "profile_with_bootstrap.yaml"
    yaml_file.write_text(
        """
extensions:
  bootstraps:
    - profile_bootstrap_demo:register
local_datasource:
  type: jsonl
  jsonl_dir: ${PROJECT_ROOT}/filtered_datasource/datasource/filtered
remote_datasource:
  type: jsonl
  jsonl_dir: ${PROJECT_ROOT}/remote_datasource/datasource
persist:
  db_path: ${PROJECT_ROOT}/runtime/test.db
logging:
  initialize: false
""".strip(),
        encoding="utf-8",
    )

    load_overrides_from_file(yaml_file, project_root=tmp_path)

    import profile_bootstrap_demo

    assert profile_bootstrap_demo.BOOTSTRAP_CALLED is False


class FileBackedConfig(BaseDataSourceConfig):
    model_config = ConfigDict(extra="forbid", validate_assignment=True)

    type: str = "file_backed"
    namespace: str = "demo"


class FileBackedDatasource(BaseDataSource):
    def __init__(self, namespace: str):
        super().__init__()
        self.namespace = namespace


def _build_file_backed_datasource(config, endpoint_name):
    return FileBackedDatasource(namespace=f"{endpoint_name}:{config.namespace}")


def test_build_config_from_file_uses_registered_datasource_type(tmp_path: Path) -> None:
    register_datasource_type(
        "file_backed",
        config_model=FileBackedConfig,
        factory=_build_file_backed_datasource,
        replace=True,
    )

    profile = tmp_path / "profile.yaml"
    profile.write_text(
        """
extensions:
  bootstraps:
    - config_from_file_bootstrap:register
local_datasource:
  type: file_backed
  namespace: local-demo
remote_datasource:
  type: jsonl
  jsonl_dir: ${PROJECT_ROOT}/remote_datasource/datasource
persist:
  db_path: ${PROJECT_ROOT}/runtime/test.db
logging:
  initialize: false
""".strip(),
        encoding="utf-8",
    )

    config = build_config_from_file(project_root=tmp_path, file_path=profile)

    assert config.local_datasource.type == "file_backed"
    assert getattr(config.local_datasource, "namespace") == "local-demo"


def test_build_config_from_sparse_profile_resolves_full_strategy_defaults(tmp_path: Path) -> None:
    profile = tmp_path / "sparse_profile.yaml"
    profile.write_text(
        """
node_types:
  - project
  - production
local_datasource:
  type: jsonl
  jsonl_dir: ${PROJECT_ROOT}/filtered_datasource/datasource/filtered
remote_datasource:
  type: jsonl
  jsonl_dir: ${PROJECT_ROOT}/remote_datasource/datasource
persist:
  db_path: ${PROJECT_ROOT}/runtime/test.db
logging:
  initialize: false
strategies:
  project:
    config:
      local_orphan_action: none
      post_check_ignore_fields:
        - code
  production:
    config:
      post_check_ignore_fields:
        - is_exist
""".strip(),
        encoding="utf-8",
    )

    config = build_config_from_file(project_root=tmp_path, file_path=profile)
    strategies = {item.node_type: item for item in config.strategies}

    project = strategies["project"].config
    assert project.auto_bind is False
    assert project.auto_bind_fields == []
    assert project.depend_fields == {"company_id": "company"}
    assert project.local_orphan_action == "none"
    assert project.remote_orphan_action == "none"
    assert project.update_direction == "push"
    assert project.post_check_ignore_fields == ["code"]
    assert project.compare_ignore_fields == []

    production = strategies["production"].config
    assert production.auto_bind_fields == ["project_id", "code"]
    assert production.depend_fields == {"project_id": "project"}
    assert production.post_check_ignore_fields == ["is_exist"]

    snapshot = config_snapshot(config)
    assert snapshot["strategies"][0]["config"]["compare_ignore_fields"] == []
    assert snapshot["strategies"][0]["config"]["field_direction_overrides"] == {}


def test_build_config_from_file_rejects_legacy_strategy_keys(tmp_path: Path) -> None:
    profile = tmp_path / "legacy_profile.yaml"
    profile.write_text(
        """
node_types:
  - supplier
local_datasource:
  type: jsonl
  jsonl_dir: ${PROJECT_ROOT}/filtered_datasource/datasource/filtered
remote_datasource:
  type: jsonl
  jsonl_dir: ${PROJECT_ROOT}/remote_datasource/datasource
persist:
  db_path: ${PROJECT_ROOT}/runtime/test.db
logging:
  initialize: false
strategies:
  supplier:
    load_mode: persisted_only
""".strip(),
        encoding="utf-8",
    )

    with pytest.raises(ValueError, match="unsupported legacy keys: load_mode"):
        build_config_from_file(project_root=tmp_path, file_path=profile)
