"""ORCH-101 (TC-03/TC-04/TC-05/TC-11, AC-1/AC-2/AC-8): host values come from
Settings, defaults equal the previous production literals, and the ORCH-058
anti-prod invariant is an executable guard.

Functional where the seam allows (launcher.agent_git_env, plane_sync link
build, image_freshness guard — monkeypatch, no network), structural where the
env dict is built inline inside a never-raise actor (self_deploy/post_deploy:
the scanner in test_no_host_hardcodes.py already proves the literals are gone;
here we pin that the replacement reads the RIGHT Settings keys).
"""

from pathlib import Path

from src.config import Settings, settings

REPO_ROOT = Path(__file__).resolve().parents[1]


# ---------------------------------------------------------------------------
# AC-2 / BR-5: defaults of the new keys equal the previous hardcoded values.
# Judged on the class fields (immune to ambient env / .env of the test host).
# ---------------------------------------------------------------------------
def test_new_settings_defaults_equal_previous_production_values():
    fields = Settings.model_fields
    assert fields["agent_home_dir"].default == "/home/slin"
    assert fields["agent_git_name"].default == "claude-bot"
    assert fields["git_email_domain"].default == "mva154.local"
    assert fields["staging_port"].default == 8501
    # Registry E (BR-5): pre-existing defaults are NOT changed by ORCH-101.
    assert fields["deploy_prod_target_port"].default == 8500
    assert fields["deploy_host_repo_path"].default == "/home/slin/repos/orchestrator"
    assert fields["host_repos_dir"].default == "/home/slin/repos"
    assert fields["deploy_ssh_user"].default == "slin"
    assert fields["gitea_owner"].default == "admin"


# ---------------------------------------------------------------------------
# TC-04: launcher.agent_git_env — single Settings-driven source for BOTH the
# agent Popen and the post-run git commit/push.
# ---------------------------------------------------------------------------
def test_agent_git_env_reads_settings(monkeypatch):
    from src.agents import launcher

    monkeypatch.setattr(launcher.settings, "agent_home_dir", "/home/deploy")
    monkeypatch.setattr(launcher.settings, "agent_git_name", "robo-bot")
    monkeypatch.setattr(launcher.settings, "git_email_domain", "newhost.example")
    env = launcher.agent_git_env()
    assert env["HOME"] == "/home/deploy"
    assert env["GIT_AUTHOR_NAME"] == "robo-bot"
    assert env["GIT_COMMITTER_NAME"] == "robo-bot"
    assert env["GIT_AUTHOR_EMAIL"] == "robo-bot@newhost.example"
    assert env["GIT_COMMITTER_EMAIL"] == "robo-bot@newhost.example"


def test_agent_git_env_default_identity_matches_previous_hardcode(monkeypatch):
    from src.agents import launcher

    # Pin the resolved values to the class defaults (ambient-env immune).
    monkeypatch.setattr(launcher.settings, "agent_home_dir", "/home/slin")
    monkeypatch.setattr(launcher.settings, "agent_git_name", "claude-bot")
    monkeypatch.setattr(launcher.settings, "git_email_domain", "mva154.local")
    env = launcher.agent_git_env()
    assert env["HOME"] == "/home/slin"
    assert env["GIT_AUTHOR_EMAIL"] == "claude-bot@mva154.local"
    assert env["GIT_COMMITTER_EMAIL"] == "claude-bot@mva154.local"


def test_agent_git_env_preserves_ambient_environ(monkeypatch):
    from src.agents import launcher

    monkeypatch.setenv("ORCH101_CANARY", "yes")
    assert launcher.agent_git_env()["ORCH101_CANARY"] == "yes"


def test_both_launcher_sites_use_the_helper():
    """Structural: the Popen env AND the post-run git env share one source."""
    src = (REPO_ROOT / "src/agents/launcher.py").read_text(encoding="utf-8")
    assert "env=agent_git_env()" in src  # agent Popen site
    assert "git_env = agent_git_env()" in src  # post-run commit/push site


# ---------------------------------------------------------------------------
# TC-05: system actors (deploy-finalizer / post-deploy-monitor) — HOME + email
# domain from Settings, actor NAMES stay platform literals (ADR-001 D2).
# ---------------------------------------------------------------------------
def test_system_actor_envs_read_settings():
    sd = (REPO_ROOT / "src/self_deploy.py").read_text(encoding="utf-8")
    pd = (REPO_ROOT / "src/post_deploy.py").read_text(encoding="utf-8")
    for source, actor in ((sd, "deploy-finalizer"), (pd, "post-deploy-monitor")):
        assert '"HOME": settings.agent_home_dir' in source
        assert f'f"{actor}@{{settings.git_email_domain}}"' in source
        assert f'"GIT_AUTHOR_NAME": "{actor}"' in source  # platform literal kept


# ---------------------------------------------------------------------------
# TC-03: plane_sync.notify_stage_change builds links from Settings
# (gitea_public_url fallback gitea_url + gitea_owner). No network: every
# outbound seam is monkeypatched.
# ---------------------------------------------------------------------------
def _capture_stage_change_msg(monkeypatch, new_stage="development"):
    import src.db as db
    from src import plane_sync

    monkeypatch.setattr(plane_sync, "_resolve_project_id", lambda wi, pid=None: "proj")
    monkeypatch.setattr(plane_sync, "update_issue_state", lambda *a, **k: None)
    sent = {}
    monkeypatch.setattr(
        plane_sync, "add_comment",
        lambda wi, msg, pid=None, author=None: sent.setdefault("msg", msg),
    )

    class _Cursor:
        def fetchone(self):
            return ("feature/ORCH-1-demo", "demo-repo")

    class _Conn:
        def execute(self, *a):
            return _Cursor()

        def close(self):
            pass

    monkeypatch.setattr(db, "get_db", lambda: _Conn())
    plane_sync.notify_stage_change("ORCH-1", "analysis", new_stage)
    return sent["msg"]


def test_stage_change_link_uses_public_url_and_owner(monkeypatch):
    from src import plane_sync

    monkeypatch.setattr(plane_sync.settings, "gitea_public_url", "https://git.example.org/")
    monkeypatch.setattr(plane_sync.settings, "gitea_owner", "acme")
    msg = _capture_stage_change_msg(monkeypatch)
    assert "https://git.example.org/acme/demo-repo/src/branch/feature/ORCH-1-demo" in msg
    assert "mva154" not in msg and "duckdns" not in msg
    assert "/admin/" not in msg  # the hardcoded owner is gone


def test_stage_change_link_falls_back_to_gitea_url(monkeypatch):
    from src import plane_sync

    monkeypatch.setattr(plane_sync.settings, "gitea_public_url", "")
    monkeypatch.setattr(plane_sync.settings, "gitea_url", "http://gitea.lan:3000")
    monkeypatch.setattr(plane_sync.settings, "gitea_owner", "owner1")
    msg = _capture_stage_change_msg(monkeypatch)
    assert "http://gitea.lan:3000/owner1/demo-repo/src/branch/" in msg


def test_stage_change_hardcoded_base_removed_from_source():
    src = (REPO_ROOT / "src/plane_sync.py").read_text(encoding="utf-8")
    assert "git.mva154.duckdns.org" not in src
    assert 'gitea_base + "/admin/"' not in src


# ---------------------------------------------------------------------------
# TC-11 / AC-8: ORCH-058 invariant — the freshness path can never be aimed at
# the prod target. staging_port is a config key (default 8501) WITH a
# fail-closed guard; service/profile names stay platform constants.
# ---------------------------------------------------------------------------
def test_staging_port_guard_refuses_prod_port(monkeypatch):
    from src import image_freshness as imf

    monkeypatch.setattr(imf.settings, "image_freshness_enabled", True)
    monkeypatch.setattr(imf.settings, "image_freshness_repos", "")
    monkeypatch.setattr(imf.settings, "staging_port", 8500)
    monkeypatch.setattr(imf.settings, "deploy_prod_target_port", 8500)
    ok, reason = imf.check_staging_image_fresh("orchestrator", "ORCH-1", "feature/x")
    assert ok is False
    assert "misconfiguration" in reason and "ORCH-058" in reason
    assert "refused" in reason  # loud refusal, no silent 8501 fallback


def test_staging_port_default_passes_guard(monkeypatch, tmp_path):
    from src import image_freshness as imf

    monkeypatch.setattr(imf.settings, "image_freshness_enabled", True)
    monkeypatch.setattr(imf.settings, "image_freshness_repos", "")
    monkeypatch.setattr(imf.settings, "staging_port", 8501)
    monkeypatch.setattr(imf.settings, "deploy_prod_target_port", 8500)
    # Point the worktree somewhere empty: the check must get PAST the guard and
    # fail-close later on the missing validated revision (proves the guard
    # itself did not fire on the default 8501/8500 split).
    monkeypatch.setattr(imf.settings, "worktrees_dir", str(tmp_path / "none"))
    monkeypatch.setattr(imf.settings, "repos_dir", str(tmp_path / "none"))
    ok, reason = imf.check_staging_image_fresh("orchestrator", "ORCH-1", "feature/x")
    assert ok is False
    assert "misconfiguration" not in reason
    assert "validated revision" in reason


def test_staging_service_names_stay_platform_constants():
    from src import image_freshness as imf

    assert imf._STAGING_SERVICE == "orchestrator-staging"
    assert imf._STAGING_COMPOSE_PROFILE == "staging"


def test_rebuild_staging_passes_configured_port_and_repo(monkeypatch):
    """D4+D7: the wired path passes TARGET_PORT from settings.staging_port and
    REPO from settings.deploy_host_repo_path EXPLICITLY (explicit-pass
    discipline of ORCH-058 kept; config is the single truth on the wired path)."""
    from src import image_freshness as imf

    monkeypatch.setattr(imf.settings, "deploy_ssh_user", "u")
    monkeypatch.setattr(imf.settings, "deploy_ssh_host", "127.0.0.1")
    monkeypatch.setattr(imf.settings, "staging_port", 9501)
    monkeypatch.setattr(imf.settings, "deploy_host_repo_path", "/srv/orchestrator")
    captured = {}

    class _R:
        returncode = 0
        stdout = ""
        stderr = ""

    def fake_run(cmd, **kw):
        captured["cmd"] = cmd
        return _R()

    monkeypatch.setattr(imf.subprocess, "run", fake_run)
    ok, _ = imf.rebuild_staging_image("orchestrator", "feature/x", "abc1234")
    assert ok is True
    inner = captured["cmd"][-1]
    assert "TARGET_PORT=9501" in inner
    assert "REPO=/srv/orchestrator" in inner
    assert "TARGET_SERVICE=orchestrator-staging" in inner


def test_build_deploy_command_passes_repo_explicitly(monkeypatch):
    """D7: the detached prod deploy passes REPO= so the hook env-override is
    actually exercised on a parametrised host (hook default = manual runs only)."""
    from src import self_deploy

    monkeypatch.setattr(self_deploy.settings, "deploy_ssh_user", "u")
    monkeypatch.setattr(self_deploy.settings, "deploy_ssh_host", "127.0.0.1")
    monkeypatch.setattr(self_deploy.settings, "deploy_host_repo_path", "/srv/orchestrator")
    cmd = self_deploy.build_deploy_command("orchestrator", "ORCH-101", "feature/x")
    remote = cmd[-1]
    assert "REPO=/srv/orchestrator" in remote
    # Exit-code contract untouched: the hook is still invoked with --deploy and
    # the wrapper still writes the result sentinel.
    assert "--deploy" in remote and "echo $?" in remote


# ---------------------------------------------------------------------------
# ADR-001 D3 anti-drift: SELF_HOSTING_REPO is a PLATFORM CONSTANT (a tirage
# convention — the platform repo MUST be named `orchestrator`), NOT a config key.
# ---------------------------------------------------------------------------
def test_self_hosting_repo_is_platform_constant():
    from src.qg.checks import SELF_HOSTING_REPO, is_self_hosting_repo

    assert SELF_HOSTING_REPO == "orchestrator"
    assert is_self_hosting_repo("orchestrator") is True
    assert is_self_hosting_repo("enduro-trails") is False
    # NOT configurable: no Settings key may claim this fact (D3 — a typo would
    # either aim deploy machinery at a foreign repo or mute all self-gates).
    assert "self_hosting_repo" not in Settings.model_fields


def test_settings_instance_importable():
    """The shared settings instance carries the new keys (smoke)."""
    assert hasattr(settings, "agent_home_dir")
    assert hasattr(settings, "agent_git_name")
    assert hasattr(settings, "git_email_domain")
    assert hasattr(settings, "staging_port")
