"""Tests for domain models and SDK-backed persistence helpers."""

from __future__ import annotations

import json
from builtins import open as builtin_open
from datetime import datetime, timedelta, timezone

import pytest

import metabranch.sdk.workspace as sdk_workspace
from metabranch.exceptions import InvalidMetabranchError, RepoLockError
from metabranch.git import GitClient
from metabranch.models import (
    BranchState,
    CheckoutResult,
    MetabranchConfigFile,
    MetabranchDefinition,
    ReconciliationPlanEntry,
    RepoLock,
    RepoLockStatus,
    StatusResult,
    WorkflowJournal,
    validate_model,
)
from metabranch.sdk import LocalState, TrackedConfig

SharedConfigStore = TrackedConfig
LocalStateStore = LocalState
from tests.helpers import git


def test_metabranch_definition_round_trip_and_legacy_parsing() -> None:
    definition = MetabranchDefinition(
        name="meta",
        base_ref="master",
        branches=("feature-one", "feature-two"),
        options={"editor": "vim"},
    )

    payload = {
        "schema_version": 1,
        "metabranch": {
            "name": definition.name,
            "base_ref": definition.base_ref,
            "branches": list(definition.branches),
            "options": dict(definition.options),
        },
    }
    assert payload["schema_version"] == 1
    assert (
        validate_model(
            payload,
            MetabranchConfigFile,
            "Invalid metabranch config payload",
        ).metabranch
        == definition
    )
    assert definition.update_branches(added=("feature-two",)) == definition
    assert (
        "feature-three" in definition.update_branches(added=("feature-three",)).branches
    )
    assert definition.update_branches(removed=("feature-one",)).branches == (
        "feature-two",
    )
    assert definition.update_branches(
        added=("feature-three", "feature-two"),
        removed=("feature-three", "feature-one"),
    ).branches == ("feature-two",)

    legacy = {
        "metabranch": {
            "name": "legacy",
            "branches": [
                {"name": "feature-one", "added_at": "2024-01-01T00:00:00Z"},
                "feature-two",
            ],
        }
    }
    assert validate_model(
        legacy,
        MetabranchConfigFile,
        "Invalid metabranch config payload",
    ).metabranch == MetabranchDefinition(
        name="legacy",
        base_ref="legacy",
        branches=("feature-one", "feature-two"),
    )
    assert validate_model(
        {"metabranch": {"name": "weird", "branches": "not-a-list", "options": "bad"}},
        MetabranchConfigFile,
        "Invalid metabranch config payload",
    ).metabranch == MetabranchDefinition(name="weird", base_ref="weird")
    assert validate_model(
        {"metabranch": {"name": "skip", "branches": [{"name": 1}, 7]}},
        MetabranchConfigFile,
        "Invalid metabranch config payload",
    ).metabranch == MetabranchDefinition(name="skip", base_ref="skip")
    assert validate_model(
        {"name": "plain-top", "branches": ["feature-one"]},
        MetabranchConfigFile,
        "Invalid metabranch config payload",
    ).metabranch == MetabranchDefinition(
        name="plain-top",
        base_ref="plain-top",
        branches=("feature-one",),
    )

    with pytest.raises(ValueError):
        validate_model(
            {"metabranch": []},
            MetabranchConfigFile,
            "Invalid metabranch config payload",
        )
    with pytest.raises(ValueError):
        validate_model(
            {"metabranch": {"branches": []}},
            MetabranchConfigFile,
            "Invalid metabranch config payload",
        )
    with pytest.raises(ValueError):
        validate_model([], MetabranchConfigFile, "Invalid metabranch config payload")
    with pytest.raises(ValueError):
        validate_model([], RepoLock, "Invalid repo lock payload")
    with pytest.raises(ValueError):
        validate_model({}, RepoLock, "Invalid repo lock payload")
    with pytest.raises(ValueError):
        validate_model([], WorkflowJournal, "Invalid workflow journal payload")
    with pytest.raises(ValueError):
        validate_model({}, WorkflowJournal, "Invalid workflow journal payload")

    plan_entry = ReconciliationPlanEntry(
        target_branch="feature-one",
        commit_sha="abc123",
        subject="Known subject",
    )
    assert plan_entry.target_branch == "feature-one"
    assert ReconciliationPlanEntry(
        action="drop",
        commit_sha="abc123",
        subject="Known subject",
    ).action == "drop"
    with pytest.raises(ValueError, match="Assign reconciliation entries require a target branch"):
        ReconciliationPlanEntry(
            commit_sha="abc123",
            subject="Known subject",
        )
    with pytest.raises(ValueError, match="Unsupported reconciliation action"):
        ReconciliationPlanEntry(
            action="skip",
            commit_sha="abc123",
            subject="Known subject",
        )

    assert CheckoutResult(branch_name="meta", is_metabranch=True).branch_name == "meta"
    status = StatusResult(
        branch_name="meta",
        is_metabranch=True,
        metabranch=MetabranchDefinition(name="meta", base_ref="master"),
        branch_states=(
            BranchState(name="feature-one", role="contained", state="available"),
        ),
    )
    assert status.statuses == ("On branch meta", "feature-one: available")

    plain_status = StatusResult(
        branch_name="feature-one",
        is_metabranch=False,
        notices=("Current branch feature-one is not a metabranch.",),
        repo_lock=RepoLockStatus(lock=RepoLock(owner="wf-1")),
    )
    assert plain_status.statuses == (
        "Current branch feature-one is not a metabranch.",
        "Repo lock: wf-1",
    )

    repo_lock = RepoLock(
        owner="wf-1",
        workflow_id="wf-1",
        operation="rebase",
        metabranch_name="meta",
    )
    assert (
        validate_model(
            repo_lock.model_dump(mode="json"),
            RepoLock,
            "Invalid repo lock payload",
        )
        == repo_lock
    )
    assert (
        RepoLock(owner="wf-naive", acquired_at="2000-01-01T00:00:00").age_seconds(
            datetime(2000, 1, 1, tzinfo=timezone.utc)
        )
        == 0
    )

    journal = WorkflowJournal(
        workflow_id="wf-1",
        workflow_type="rebase",
        metabranch_name="meta",
        status="running",
        current_step="merge:feature-one",
        args={"base": "master"},
        checkpoints={"ref:meta": "abc123"},
    )
    payload = journal.model_dump(mode="json")
    assert (
        validate_model(payload, WorkflowJournal, "Invalid workflow journal payload")
        == journal
    )
    updated = journal.with_updates(status="blocked", current_step="resume")
    assert updated.status == "blocked"
    assert updated.current_step == "resume"
    assert updated.updated_at != journal.updated_at


def test_shared_config_store_and_local_state(monkeypatch, repo_with_branches) -> None:
    monkeypatch.chdir(repo_with_branches)
    git_client = GitClient()
    config_store = SharedConfigStore(git_client)
    local_state = LocalStateStore(git_client)
    definition = MetabranchDefinition(
        name="meta",
        base_ref="master",
        branches=("feature-one", "feature-two"),
    )

    assert config_store.path == repo_with_branches / ".metabranch.yaml"
    assert config_store.exists() is False
    with pytest.raises(InvalidMetabranchError):
        config_store.load()

    git_client.checkout("meta", create=True)
    config_store.save(definition)
    assert config_store.exists() is True
    assert config_store.load() == definition
    git(repo_with_branches, "add", ".metabranch.yaml")
    git(repo_with_branches, "commit", "-m", "Add metabranch config")

    git_client.checkout("master")
    assert config_store.exists_in_branch("meta") is True
    assert config_store.exists_in_branch("feature-one") is False
    assert config_store.load_from_branch("meta") == definition
    assert config_store.load_from_branch("feature-one") is None

    assert local_state.load_registry() == {}
    local_state.register(definition)
    assert local_state.list_names() == ["meta"]
    assert local_state.get_definition("missing") is None
    assert local_state.get_definition("meta") == definition
    local_state.set_active("meta", workflow_id="wf-1")
    local_state.unregister("meta")
    assert local_state.list_names() == []
    assert local_state.get_definition("meta") is None
    assert local_state.get_active() is None
    assert local_state.get_active_workflow_id() == "wf-1"
    local_state.set_active("meta", workflow_id="wf-1")
    local_state.register(definition)
    assert local_state.get_active() == "meta"
    assert local_state.get_active_workflow_id() == "wf-1"

    registry = json.loads(local_state.registry_path.read_text())
    assert registry["meta"]["branches"] == ["feature-one", "feature-two"]
    local_state.save_registry({"broken": {"base_ref": 1, "branches": "bad"}})
    assert local_state.get_definition("broken") is None

    journal = WorkflowJournal(
        workflow_id="wf-1",
        workflow_type="pull",
        metabranch_name="meta",
        checkpoints={"ref:meta": "abc123"},
    )
    local_state.save_workflow(journal)
    assert local_state.load_workflow("wf-1") == journal
    assert local_state.get_active_workflow() == journal
    local_state.set_active_workflow(None)
    assert local_state.get_active_workflow() is None
    assert local_state.load_workflow("missing") is None

    local_state.workflow_path("broken").write_text(json.dumps(["unexpected"]))
    assert local_state.load_workflow("broken") is None
    local_state.workflow_path("invalid").write_text(json.dumps({"workflow_id": "wf-2"}))
    assert local_state.load_workflow("invalid") is None

    local_state.set_active("meta", workflow_id="wf-1")
    repo_lock = local_state.acquire_repo_lock(
        owner="wf-1",
        workflow_id="wf-1",
        operation="rebase",
        metabranch_name="meta",
    )
    assert local_state.load_repo_lock() == repo_lock
    assert local_state.inspect_repo_lock() == RepoLockStatus(lock=repo_lock)
    assert local_state.acquire_repo_lock(owner="wf-1") == repo_lock
    with pytest.raises(RepoLockError):
        local_state.acquire_repo_lock(owner="wf-2")
    with pytest.raises(RepoLockError):
        local_state.release_repo_lock("wf-2")
    local_state.release_repo_lock("wf-1")
    assert local_state.load_repo_lock() is None
    assert local_state.inspect_repo_lock() is None
    local_state.release_repo_lock()

    local_state.active_path.write_text(json.dumps(["unexpected"]))
    assert local_state.get_active() is None
    assert local_state.get_active_workflow_id() is None
    local_state.active_path.unlink()
    assert local_state.get_active() is None

    local_state.repo_lock_path.write_text(json.dumps(["unexpected"]))
    assert local_state.load_repo_lock() is None
    assert local_state.inspect_repo_lock() is None
    local_state.repo_lock_path.write_text(json.dumps({"workflow_id": "wf-1"}))
    assert local_state.load_repo_lock() is None
    assert local_state.inspect_repo_lock() is None

    tmp_file = local_state.write_tmp_file("plan.txt", "hello\n")
    assert tmp_file.read_text() == "hello\n"


def test_local_state_repo_lock_race_surfaces_existing_owner(
    monkeypatch, repo_with_branches
) -> None:
    monkeypatch.chdir(repo_with_branches)
    local_state = LocalStateStore(GitClient())
    raced_lock = RepoLock(owner="wf-race")

    def fake_open(path, mode="r", *args, **kwargs):
        if path == local_state.repo_lock_path and mode == "x":
            with builtin_open(path, "w") as handle:
                json.dump(
                    raced_lock.model_dump(mode="json"),
                    handle,
                )
            raise FileExistsError
        return builtin_open(path, mode, *args, **kwargs)

    monkeypatch.setattr(sdk_workspace, "open", fake_open, raising=False)

    with pytest.raises(RepoLockError, match="wf-race"):
        local_state.acquire_repo_lock(owner="wf-1")


def test_local_state_repo_lock_race_allows_same_owner(
    monkeypatch, repo_with_branches
) -> None:
    monkeypatch.chdir(repo_with_branches)
    local_state = LocalStateStore(GitClient())
    raced_lock = RepoLock(owner="wf-1")

    def fake_open(path, mode="r", *args, **kwargs):
        if path == local_state.repo_lock_path and mode == "x":
            with builtin_open(path, "w") as handle:
                json.dump(
                    raced_lock.model_dump(mode="json"),
                    handle,
                )
            raise FileExistsError
        return builtin_open(path, mode, *args, **kwargs)

    monkeypatch.setattr(sdk_workspace, "open", fake_open, raising=False)

    repo_lock = local_state.acquire_repo_lock(owner="wf-1")
    assert repo_lock.owner == "wf-1"


def test_local_state_repo_lock_race_surfaces_stale_lock_guidance(
    monkeypatch, repo_with_branches
) -> None:
    monkeypatch.chdir(repo_with_branches)
    local_state = LocalStateStore(GitClient())
    raced_lock = RepoLock(
        owner="wf-race",
        operation="push",
        acquired_at="2000-01-01T00:00:00+00:00",
    )

    def fake_open(path, mode="r", *args, **kwargs):
        if path == local_state.repo_lock_path and mode == "x":
            with builtin_open(path, "w") as handle:
                json.dump(
                    raced_lock.model_dump(mode="json"),
                    handle,
                )
            raise FileExistsError
        return builtin_open(path, mode, *args, **kwargs)

    monkeypatch.setattr(sdk_workspace, "open", fake_open, raising=False)

    with pytest.raises(RepoLockError, match="stale metabranch repo lock"):
        local_state.acquire_repo_lock(owner="wf-1")


def test_local_state_detects_stale_workflow_and_short_operation_locks(
    monkeypatch, repo_with_branches
) -> None:
    monkeypatch.chdir(repo_with_branches)
    local_state = LocalStateStore(GitClient())

    completed_journal = WorkflowJournal(
        workflow_id="wf-1",
        workflow_type="rebase",
        metabranch_name="meta",
        status="completed",
    )
    local_state.save_workflow(completed_journal)
    local_state.set_active("meta", workflow_id="wf-1")
    local_state.acquire_repo_lock(
        owner="wf-1",
        workflow_id="wf-1",
        operation="rebase",
        metabranch_name="meta",
    )
    completed_status = local_state.inspect_repo_lock()
    assert completed_status is not None
    assert completed_status.is_stale is True
    assert completed_status.reason == "workflow is already completed"
    local_state.release_repo_lock("wf-1")

    missing_journal_lock = RepoLock(
        owner="wf-2",
        workflow_id="wf-2",
        operation="rebase",
        metabranch_name="meta",
    )
    local_state.repo_lock_path.write_text(
        json.dumps(missing_journal_lock.model_dump(mode="json"))
    )
    missing_status = local_state.inspect_repo_lock()
    assert missing_status is not None
    assert missing_status.is_stale is True
    assert missing_status.reason == "workflow journal is missing"
    local_state.release_repo_lock()

    running_journal = WorkflowJournal(
        workflow_id="wf-3",
        workflow_type="rebase",
        metabranch_name="meta",
        status="running",
    )
    local_state.save_workflow(running_journal)
    local_state.set_active("meta", workflow_id="wf-other")
    local_state.repo_lock_path.write_text(
        json.dumps(
            RepoLock(
                owner="wf-3",
                workflow_id="wf-3",
                operation="rebase",
                metabranch_name="meta",
            ).model_dump(mode="json")
        )
    )
    mismatch_status = local_state.inspect_repo_lock()
    assert mismatch_status is not None
    assert mismatch_status.is_stale is True
    assert mismatch_status.reason == "active workflow pointer does not match lock owner"
    local_state.release_repo_lock()

    old_lock = RepoLock(
        owner="short-op",
        operation="push",
        acquired_at=(datetime.now(timezone.utc) - timedelta(minutes=10)).isoformat(),
    )
    local_state.repo_lock_path.write_text(json.dumps(old_lock.model_dump(mode="json")))
    short_status = local_state.inspect_repo_lock()
    assert short_status is not None
    assert short_status.is_stale is True
    assert "short operation lock exceeded" in short_status.reason
