"""W4P activation from a project's CI run: the run is verified, the commit comes from the run's token,
the version is the one that deployment produces, and the route moves only when every check passes.
Repositories and the W4P provider are fakes."""

import time
from datetime import datetime, timezone
from types import SimpleNamespace
from uuid import uuid4

import pytest
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from jose import jwt
from jose.backends import RSAKey

from src.config import Settings
from src.domain.hosting import w4p_activation as act
from src.domain.hosting.adapters.w4p_static import version_script_name
from src.domain.hosting.ci_activation import WAITING_FOR_CI_STEP, CiActivationError

AUD = "p2hs-w4p-activate"
REPO = "Pie-Solutions/proj-repo"
SHA = "c" * 40
REF = "pbvdnw64"

_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
_pem = _key.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption())
_pub = _key.public_key().public_bytes(serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo)
JWKS = {"keys": [RSAKey(_pub, "RS256").to_dict()]}


def _token(**overrides):
    claims = {
        "iss": "https://token.actions.githubusercontent.com",
        "aud": AUD,
        "repository": REPO,
        "sha": SHA,
        "exp": int(time.time()) + 300,
    }
    claims.update(overrides)
    return jwt.encode(claims, _pem, algorithm="RS256")


async def _jwks():
    return JWKS


async def _healthy(_hostname):
    return "healthy"


def _settings(**overrides) -> Settings:
    base = dict(
        new_cloudflare_account_id="b6152b2d03f9bbcc29e0023a294163d5",
        new_cloudflare_api_token="PLATFORM",
        new_hosting_public_apex="p2hs.dev",
        new_cloudflare_dispatch_kv_namespace_id="PLATFORM-KV",
        w4p_activation_audience=AUD,
        hosting_platform_projects_created_after="2026-10-08T00:00:00+00:00",
        hosting_public_apex="p2hsapp.com",
        cloudflare_account_id="8551c80e9c3a0422d2fef6e2800d6143",
        cloudflare_api_token="GLOBAL",
        cloudflare_dispatch_kv_namespace_id="GLOBAL-KV",
    )
    base.update(overrides)
    return Settings(**base)


class FakeSession:
    def __init__(self):
        self.commits = 0

    async def commit(self):
        self.commits += 1


class FakeDeployRepo:
    deployments = []
    outcomes = []

    def __init__(self, _db):
        pass

    async def list_for_project(self, _project_id):
        return list(FakeDeployRepo.deployments)

    async def apply_hosting_outcome(self, _id, **kwargs):
        FakeDeployRepo.outcomes.append(kwargs)
        deployment = next(d for d in FakeDeployRepo.deployments if d.id == _id)
        for key, value in kwargs.items():
            if hasattr(deployment, key):
                setattr(deployment, key, value)
        if "metadata_patch" in kwargs:
            deployment.deployment_metadata.update(kwargs["metadata_patch"])

    async def create(self, project_id, revision_id, environment, created_by=None, deployment_metadata=None):
        deployment = SimpleNamespace(
            id=uuid4(), project_id=project_id, revision_id=revision_id,
            status="pending", last_completed_step=None, environment=environment,
            source_git_sha=None, deployment_metadata=deployment_metadata or {},
        )
        FakeDeployRepo.deployments.insert(0, deployment)
        return deployment


class FakeProjectRepo:
    project = None
    hostnames = []

    def __init__(self, _db):
        pass

    async def get_by_project_ref(self, ref):
        return FakeProjectRepo.project if ref == REF else None

    async def get_with_repo(self, _id):
        return FakeProjectRepo.project

    async def update_hostnames(self, _id, **kwargs):
        FakeProjectRepo.hostnames.append(kwargs)


class FakeRevisionRepo:
    revisions = []

    def __init__(self, _db):
        pass

    async def get_by_git_sha(self, _project_id, git_sha):
        return next((r for r in self.revisions if r.git_sha == git_sha), None)

    async def get_by_source(self, _project_id, source):
        return next((r for r in self.revisions if r.source == source), None)

    async def create(self, project_id, git_sha, source, source_id=None, commit_message=None, created_by=None):
        revision = SimpleNamespace(
            id=uuid4(), project_id=project_id, git_sha=git_sha, source=source,
            source_id=source_id, commit_message=commit_message,
        )
        self.revisions.insert(0, revision)
        return revision


class FakeProvider:
    calls = []
    secrets = []

    def __init__(self, ok=True):
        self.ok = ok

    async def bind_runtime_secrets(self, script_name, secrets):
        FakeProvider.secrets.append((script_name, secrets))

    async def activate_deployment(self, ctx, version, live, hostname, descriptor):
        FakeProvider.calls.append((version, live, hostname, descriptor))
        return SimpleNamespace(ok=self.ok, message="" if self.ok else "kv_failed")


class FakePreviewRepo:
    previews = []
    updates = []

    def __init__(self, _db):
        pass

    async def get_latest_for_revision(self, _project_id, revision_id):
        return next((p for p in self.previews if p.revision_id == revision_id), None)

    async def list_for_project(self, _project_id):
        return list(self.previews)

    async def update_status(self, preview_id, status, **kwargs):
        self.updates.append((preview_id, status, kwargs))


class FakeInfra:
    def __init__(self, **_kwargs):
        pass

    async def get_credentials_for_workspace(self, _ws):
        return SimpleNamespace(account_id="b6152b2d03f9bbcc29e0023a294163d5", api_token="PLATFORM", zone_id="",
                               dispatch_kv_namespace_id="PLATFORM-KV", w4p_namespace_production="p2hs-production",
                               w4p_namespace_preview="p2hs-previews", w4p_namespace_staging="p2hs-staging")


@pytest.fixture
def wired(monkeypatch):
    FakeDeployRepo.outcomes = []
    FakeProjectRepo.hostnames = []
    FakeProvider.calls = []
    FakeProvider.secrets = []
    project_id = uuid4()
    FakeRevisionRepo.revisions = [SimpleNamespace(
        id=uuid4(), project_id=project_id,
        git_sha="a" * 40, source="codegen", source_id=uuid4(),
    )]
    FakeDeployRepo.deployments = [SimpleNamespace(
        id=uuid4(), project_id=project_id, revision_id=uuid4(), status="deploying",
        last_completed_step=WAITING_FOR_CI_STEP, environment="production", source_git_sha=SHA,
        deployment_metadata={},
    )]
    FakeProjectRepo.project = SimpleNamespace(
        id=project_id, project_ref=REF, workspace_id=uuid4(), type="nuxt_app",
        created_at=datetime(2026, 10, 9, tzinfo=timezone.utc),
        repo=SimpleNamespace(repo_full_name=REPO, default_branch="main"),
    )
    monkeypatch.setattr(act, "DeployRepository", FakeDeployRepo)
    monkeypatch.setattr(act, "RevisionRepository", FakeRevisionRepo)
    monkeypatch.setattr(act, "ProjectRepository", FakeProjectRepo)
    monkeypatch.setattr(act, "get_w4p_static_provider", lambda _s, **_kwargs: FakeProvider())
    monkeypatch.setattr(act, "CloudInfraService", FakeInfra)


def _run(settings=None, token=None, version=None, ref=REF):
    version = version or version_script_name(f"p2hs-prod-{REF}", SHA)
    return act.activate_from_ci(
        FakeSession(), project_ref=ref, version_script=version,
        bearer=token or _token(), settings=settings or _settings(), fetch_jwks=_jwks,
        runtime_health=_healthy,
    )


def test_new_hostname_uses_manifest_and_an_existing_route_keeps_its_policy():
    from src.domain.hosting.w4p_activation import routing_policy_for_activation

    assert routing_policy_for_activation(None) == "manifest"
    assert routing_policy_for_activation("") == "manifest"
    assert routing_policy_for_activation("{}") == "legacy"
    assert routing_policy_for_activation('{"routing_policy": "legacy"}') == "legacy"
    assert routing_policy_for_activation('{"routing_policy": "manifest"}') == "manifest"


@pytest.mark.asyncio
async def test_valid_run_moves_the_route_and_marks_the_deployment_live(wired):
    out = await _run(settings=_settings(cloud_public_api_url="https://cloud.example/"))
    assert out["ok"] is True
    version, live, hostname, descriptor = FakeProvider.calls[0]
    assert version == f"p2hs-prod-{REF}-v{SHA[:8]}"
    assert hostname == f"proj-{REF}.p2hsapp.com"
    assert descriptor["bff_origin"] == "https://cloud.example"
    assert descriptor["api_execution"] == "project_runtime"
    assert descriptor["routing_policy"] == "manifest"
    assert FakeProvider.secrets[0][1]["CLOUD_GATEWAY_URL"] == "https://cloud.example"
    assert "PROJECT_ID" in FakeProvider.secrets[0][1]
    assert FakeDeployRepo.outcomes[-1]["status"] == "live"
    assert FakeDeployRepo.outcomes[-1]["hosting_provider"] == "cloudflare_w4p_static"
    assert FakeProjectRepo.hostnames == [{"live_hostname": hostname}]


@pytest.mark.asyncio
async def test_same_sha_in_preview_and_production_uses_requested_environment(wired):
    # Preview and production workflows can upload the same commit concurrently.
    # Activation must not select whichever deployment happens to be newest.
    preview = SimpleNamespace(
        id=uuid4(), project_id=FakeProjectRepo.project.id, revision_id=uuid4(),
        status="deploying", last_completed_step=WAITING_FOR_CI_STEP,
        environment="preview", source_git_sha=SHA,
    )
    FakeDeployRepo.deployments.insert(0, preview)

    out = await _run()

    assert out["ok"] is True
    assert FakeDeployRepo.outcomes[-1]["status"] == "live"
    assert FakeDeployRepo.outcomes[-1]["source_git_sha"] == SHA
    assert FakeProvider.calls[0][1] == f"p2hs-prod-{REF}"


@pytest.mark.asyncio
async def test_disabled_until_an_audience_is_configured(wired):
    with pytest.raises(CiActivationError, match="activation_not_configured"):
        await _run(settings=_settings(w4p_activation_audience=""))
    assert FakeProvider.calls == []


@pytest.mark.asyncio
async def test_an_unknown_project_ref_is_refused(wired):
    with pytest.raises(CiActivationError, match="project_not_found"):
        await _run(ref="nope0000")
    assert FakeProvider.calls == []


@pytest.mark.asyncio
async def test_a_run_for_a_commit_with_no_deployment_is_refused(wired):
    with pytest.raises(CiActivationError, match="deployment_not_found"):
        await _run(token=_token(sha="d" * 40))
    assert FakeProvider.calls == []


@pytest.mark.asyncio
async def test_preview_activation_recovers_when_workflow_webhook_was_missed(wired):
    token_sha = "d" * 40
    out = await _run(
        token=_token(sha=token_sha),
        version=version_script_name(f"p2hs-prev-{REF}", token_sha),
    )

    assert out["ok"] is True
    created = next(d for d in FakeDeployRepo.deployments if d.source_git_sha == token_sha)
    assert created.status == "live"
    assert created.last_completed_step == "routing_activated"
    assert FakeDeployRepo.outcomes[-1]["status"] == "live"
    assert any(r.git_sha == token_sha for r in FakeRevisionRepo.revisions)


@pytest.mark.asyncio
async def test_bootstrap_deployment_can_adopt_a_repaired_ci_sha(wired):
    FakeDeployRepo.deployments[0].source_git_sha = "a" * 40
    FakeDeployRepo.deployments[0].deployment_metadata = {"bootstrap_initial_hosting": True}
    out = await _run()
    assert out["ok"] is True
    assert FakeDeployRepo.deployments[0].source_git_sha == SHA


@pytest.mark.asyncio
async def test_a_deployment_not_waiting_for_the_run_cannot_be_activated(wired):
    FakeDeployRepo.deployments[0].last_completed_step = "routing_activated"
    with pytest.raises(CiActivationError, match="deployment_not_waiting"):
        await _run()
    assert FakeProvider.calls == []


@pytest.mark.asyncio
async def test_exact_sha_live_deployment_can_be_reactivated_idempotently(wired):
    FakeDeployRepo.deployments[0].status = "live"
    FakeDeployRepo.deployments[0].last_completed_step = "routing_activated"

    out = await _run()

    assert out["ok"] is True
    assert FakeProvider.calls
    assert FakeDeployRepo.outcomes[-1]["status"] == "live"


@pytest.mark.asyncio
async def test_a_version_that_is_not_this_deployments_version_is_refused(wired):
    with pytest.raises(CiActivationError, match="version_script_mismatch"):
        await _run(version=f"p2hs-prod-{REF}-vdeadbeef")
    assert FakeProvider.calls == []


@pytest.mark.asyncio
async def test_a_run_from_another_repository_is_refused_before_any_route_change(wired):
    with pytest.raises(CiActivationError, match="repository_mismatch"):
        await _run(token=_token(repository="Pie-Solutions/other"))
    assert FakeProvider.calls == []


@pytest.mark.asyncio
async def test_missing_account_kv_fails_closed_and_does_not_fall_back(wired):
    with pytest.raises(CiActivationError, match="account_kv_not_configured"):
        await _run(settings=_settings(cloudflare_dispatch_kv_namespace_id=""))
    assert FakeProvider.calls == []


@pytest.mark.asyncio
async def test_a_project_created_before_the_cutoff_uses_its_own_account_and_apex(wired):
    FakeProjectRepo.project.created_at = datetime(2026, 9, 1, tzinfo=timezone.utc)
    out = await _run(settings=_settings(new_cloudflare_dispatch_kv_namespace_id=""))
    assert out["ok"] is True
    version, live, hostname, _ = FakeProvider.calls[0]
    assert hostname == f"proj-{REF}.p2hsapp.com"


@pytest.mark.asyncio
async def test_a_project_on_the_platform_uses_the_platform_apex(wired):
    out = await _run()
    assert out["ok"] is True
    assert FakeProvider.calls[0][2] == f"proj-{REF}.p2hsapp.com"


class _RouteProvider(FakeProvider):
    def __init__(self):
        super().__init__()
        self.restores = []

    async def read_route(self, hostname):
        return None

    async def restore_route(self, hostname, raw):
        self.restores.append((hostname, raw))


class _EmptyDomains:
    def __init__(self, _db):
        pass

    async def list_verified_for_environment(self, *_args, **_kwargs):
        return []


@pytest.mark.asyncio
async def test_pending_health_keeps_the_new_route(wired, monkeypatch):
    provider = _RouteProvider()
    monkeypatch.setattr(act, "get_w4p_static_provider", lambda _s, **_kwargs: provider)
    monkeypatch.setattr("src.domain.deployments.repository.DomainRepository", _EmptyDomains)

    async def _pending(_hostname):
        return "pending"

    out = await act.activate_from_ci(
        FakeSession(), project_ref=REF, version_script=version_script_name(f"p2hs-prod-{REF}", SHA),
        bearer=_token(), settings=_settings(), fetch_jwks=_jwks, runtime_health=_pending,
    )
    assert out["ok"] is True
    assert provider.calls
    assert provider.restores == []
    assert FakeDeployRepo.outcomes[-1]["status"] == "live"


@pytest.mark.asyncio
async def test_unhealthy_runtime_restores_the_previous_route(wired, monkeypatch):
    provider = _RouteProvider()
    monkeypatch.setattr(act, "get_w4p_static_provider", lambda _s, **_kwargs: provider)
    monkeypatch.setattr("src.domain.deployments.repository.DomainRepository", _EmptyDomains)

    async def _unhealthy(_hostname):
        return "unhealthy"

    with pytest.raises(CiActivationError, match="runtime_health_failed"):
        await act.activate_from_ci(
            FakeSession(), project_ref=REF, version_script=version_script_name(f"p2hs-prod-{REF}", SHA),
            bearer=_token(), settings=_settings(), fetch_jwks=_jwks, runtime_health=_unhealthy,
        )
    assert len(provider.restores) == 1


def test_activation_errors_the_workflow_retries_are_not_a_cloudflare_502():
    assert act._STATUS_FOR_CODE["runtime_health_failed"] == 503
    assert act._STATUS_FOR_CODE["routing_write_failed"] == 503


@pytest.mark.asyncio
async def test_a_failed_route_write_leaves_the_deployment_unchanged(wired, monkeypatch):
    monkeypatch.setattr(act, "get_w4p_static_provider", lambda _s, **_kwargs: FakeProvider(ok=False))
    with pytest.raises(CiActivationError, match="kv_failed"):
        await _run()
    assert all(o.get("status") != "live" for o in FakeDeployRepo.outcomes)


@pytest.mark.asyncio
async def test_activation_finalizes_pending_preview_when_revision_link_is_stale(monkeypatch):
    FakePreviewRepo.previews = [
        SimpleNamespace(id=uuid4(), revision_id=uuid4(), status="provisioning"),
    ]
    FakePreviewRepo.updates = []
    monkeypatch.setattr("src.domain.previews.repository.PreviewRepository", FakePreviewRepo)
    db = FakeSession()
    await act._finalize_waiting_preview(
        db,
        uuid4(),
        uuid4(),
        "https://preview.example/",
    )
    assert FakePreviewRepo.updates == [
        (FakePreviewRepo.previews[0].id, "running", {
            "base_url": "https://preview.example/",
            "provider_preview_url": "https://preview.example/",
        }),
    ]
    assert db.commits == 1
