"""Tests for exokit.core.deployer — calls scaffolded deploy scripts."""

from __future__ import annotations

import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch

from exokit.core.deployer import (
    DeployResult,
    ResourceLink,
    _extract_urls,
    _run_script,
    deploy_hello_world,
)

# ---------------------------------------------------------------------------
# DeployResult / ResourceLink
# ---------------------------------------------------------------------------


class TestDeployResult:
    def test_default(self) -> None:
        r = DeployResult()
        assert r.success is True
        assert r.resources == []
        assert r.warnings == []

    def test_add_resource(self) -> None:
        r = DeployResult()
        r.add("Data Gen", "job", "https://host/jobs/1", "success")
        assert len(r.resources) == 1
        assert r.resources[0].name == "Data Gen"
        assert r.resources[0].kind == "job"


class TestResourceLink:
    def test_fields(self) -> None:
        rl = ResourceLink(name="Test", kind="job", url="https://x", status="ok")
        assert rl.name == "Test"

    def test_default_status(self) -> None:
        rl = ResourceLink(name="Test", kind="job", url="")
        assert rl.status == "unknown"


# ---------------------------------------------------------------------------
# _extract_urls
# ---------------------------------------------------------------------------


class TestExtractUrls:
    def test_extracts_job_url(self) -> None:
        output = "Run URL: https://host/#job/456/runs/789\nDone."
        urls = _extract_urls(output)
        assert "job" in urls
        assert "#job/456" in urls["job"]

    def test_extracts_pipeline_url(self) -> None:
        output = "See https://host/pipelines/abc-def"
        urls = _extract_urls(output)
        assert "pipeline" in urls

    def test_extracts_genie_url(self) -> None:
        output = "URL: https://host/genie/rooms/g-123"
        urls = _extract_urls(output)
        assert "genie" in urls
        assert "g-123" in urls["genie"]

    def test_no_urls(self) -> None:
        assert _extract_urls("No URLs here") == {}

    def test_empty_string(self) -> None:
        assert _extract_urls("") == {}


# ---------------------------------------------------------------------------
# _run_script
# ---------------------------------------------------------------------------


class TestRunScript:
    @patch("exokit.core.deployer.subprocess.run")
    def test_runs_bash_script(self, mock_run: MagicMock, tmp_path: Path) -> None:
        script = tmp_path / "deploy.sh"
        script.write_text("#!/bin/bash\necho ok")
        mock_run.return_value = subprocess.CompletedProcess(
            args=[], returncode=0, stdout="ok", stderr=""
        )
        result = _run_script(script, ["dev"], cwd=tmp_path)
        assert result.returncode == 0
        cmd = mock_run.call_args[0][0]
        assert cmd[0] == "bash"
        assert "dev" in cmd

    @patch("exokit.core.deployer.subprocess.run")
    def test_runs_python_script(self, mock_run: MagicMock, tmp_path: Path) -> None:
        script = tmp_path / "setup.py"
        script.write_text("print('ok')")
        mock_run.return_value = subprocess.CompletedProcess(
            args=[], returncode=0, stdout="ok", stderr=""
        )
        result = _run_script(script, cwd=tmp_path)
        assert result.returncode == 0
        cmd = mock_run.call_args[0][0]
        assert cmd[0] == "python3"


# ---------------------------------------------------------------------------
# deploy_hello_world
# ---------------------------------------------------------------------------


class TestDeployHelloWorld:
    def _setup_scripts(self, tmp_path: Path) -> None:
        """Create the expected script files."""
        scripts = tmp_path / "scripts"
        scripts.mkdir(parents=True)
        (scripts / "deploy-lakehouse.sh").write_text("#!/bin/bash\necho ok")
        (scripts / "deploy-app.sh").write_text("#!/bin/bash\necho ok")

    @patch("exokit.core.deployer._run_script")
    def test_full_success(self, mock_script: MagicMock, tmp_path: Path) -> None:
        self._setup_scripts(tmp_path)

        # Lakehouse script succeeds with all completion markers
        lakehouse_out = (
            "Bundle deployed.\n"
            "Data generation complete.\n"
            "DLT pipeline complete.\n"
            "ML training complete.\n"
            "Genie Space created: g-1\n"
            "URL: https://host/genie/rooms/g-1\n"
        )
        app_out = "App deployment complete!\n"

        mock_script.side_effect = [
            subprocess.CompletedProcess(args=[], returncode=0, stdout=lakehouse_out, stderr=""),
            subprocess.CompletedProcess(args=[], returncode=0, stdout=app_out, stderr=""),
        ]

        statuses: list[tuple[int, str]] = []
        result = deploy_hello_world(
            tmp_path,
            bundle_name="test-demo",
            app_name="test-app",
            catalog="cat",
            warehouse_id="wh",
            workspace_host="https://host",
            on_status=lambda s, m: statuses.append((s, m)),
        )

        assert result.success is True
        assert len(result.resources) >= 4
        assert len(statuses) == 2
        assert mock_script.call_count == 2

    @patch("exokit.core.deployer._run_script")
    def test_lakehouse_failure_aborts(self, mock_script: MagicMock, tmp_path: Path) -> None:
        self._setup_scripts(tmp_path)

        mock_script.return_value = subprocess.CompletedProcess(
            args=[], returncode=1, stdout="Bundle deployed.\n", stderr="Error: pipeline failed"
        )

        result = deploy_hello_world(
            tmp_path,
            bundle_name="test-demo",
            app_name="test-app",
            catalog="cat",
            warehouse_id="wh",
            workspace_host="https://host",
        )

        assert result.success is False
        assert any("deploy-lakehouse.sh failed" in w for w in result.warnings)
        # Should still have parsed the successful bundle deploy
        assert any(r.name == "Bundle Deploy" for r in result.resources)

    @patch("exokit.core.deployer._run_script")
    def test_app_failure_is_warning(self, mock_script: MagicMock, tmp_path: Path) -> None:
        self._setup_scripts(tmp_path)

        mock_script.side_effect = [
            subprocess.CompletedProcess(
                args=[],
                returncode=0,
                stdout="Bundle deployed.\nData generation complete.\n",
                stderr="",
            ),
            subprocess.CompletedProcess(
                args=[], returncode=1, stdout="", stderr="active deployment in progress"
            ),
        ]

        result = deploy_hello_world(
            tmp_path,
            bundle_name="test-demo",
            app_name="test-app",
            catalog="cat",
            warehouse_id="wh",
            workspace_host="https://host",
        )

        # Lakehouse succeeded so overall not necessarily failed
        assert any(r.kind == "app" and r.status == "failed" for r in result.resources)

    @patch("exokit.core.deployer._run_script")
    def test_empty_app_name_skips_app(self, mock_script: MagicMock, tmp_path: Path) -> None:
        self._setup_scripts(tmp_path)

        mock_script.return_value = subprocess.CompletedProcess(
            args=[], returncode=0, stdout="Lakehouse deployment complete!\n", stderr=""
        )

        result = deploy_hello_world(
            tmp_path,
            bundle_name="test-demo",
            app_name="",
            catalog="cat",
            warehouse_id="wh",
            workspace_host="https://host",
        )

        assert any("skipped" in w.lower() for w in result.warnings)
        # Only called once (lakehouse, not app)
        assert mock_script.call_count == 1

    def test_missing_lakehouse_script(self, tmp_path: Path) -> None:
        result = deploy_hello_world(
            tmp_path,
            bundle_name="test-demo",
            app_name="test-app",
            catalog="cat",
            warehouse_id="wh",
            workspace_host="https://host",
        )

        assert result.success is False
        assert any("not found" in w for w in result.warnings)
