"""Tests for the APX bridge module.

All tests mock subprocess.run to avoid requiring the actual APX CLI.
"""

from __future__ import annotations

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

import pytest
import yaml

from exokit.core.apx_bridge import (
    _BACKEND_LAYERS,
    APXNotInstalledError,
    APXScaffoldError,
    APXTimeoutError,
    _apply_governance_overlay,
    _postprocess_app_env,
    _postprocess_apx_config,
    scaffold_app,
)
from exokit.core.apx_content import APP_CLAUDE_MD, write_app_readme
from exokit.core.models import ScaffoldContext

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture()
def context() -> ScaffoldContext:
    """Minimal ScaffoldContext for APX bridge tests."""
    return ScaffoldContext(
        company="Acme Bank",
        industry="fsi",
        demo_description="Test demo description",
        catalog="acme_bank_fsi",
        warehouse_id="abc123def456",
        workspace_host="https://adb-1234567890.azuredatabricks.net",
        databricks_profile="DEFAULT",
        notification_email="demo@acmebank.com",
        bundle_name="acme-bank-fsi-demo",
        app_name="acme-bank-fsi-app",
    )


@pytest.fixture()
def context_no_profile() -> ScaffoldContext:
    """ScaffoldContext without a databricks_profile."""
    return ScaffoldContext(
        company="Acme Bank",
        industry="fsi",
        demo_description="Test demo description",
        catalog="acme_bank_fsi",
        warehouse_id="abc123def456",
        workspace_host="https://adb-1234567890.azuredatabricks.net",
        databricks_profile=None,
        notification_email="demo@acmebank.com",
        bundle_name="acme-bank-fsi-demo",
        app_name="acme-bank-fsi-app",
    )


def _create_apx_layout(app_dir: Path, app_slug: str = "acme_bank_fsi_app") -> Path:
    """Create the standard APX directory layout and return the backend path."""
    backend = app_dir / "src" / app_slug / "backend"
    backend.mkdir(parents=True)
    ui = app_dir / "src" / app_slug / "ui"
    ui.mkdir(parents=True)
    return backend


# ---------------------------------------------------------------------------
# scaffold_app — happy path
# ---------------------------------------------------------------------------


class TestScaffoldAppHappyPath:
    """Tests for successful APX scaffold + governance overlay."""

    @patch("exokit.core.apx_bridge.subprocess.run")
    def test_calls_apx_with_correct_flags(
        self, mock_run: MagicMock, context: ScaffoldContext, tmp_path: Path
    ) -> None:
        """Verify the subprocess command includes name, addons, profile, and path."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=[], returncode=0, stdout="App scaffolded", stderr=""
        )
        app_dir = tmp_path / "apps" / "main-app"
        _create_apx_layout(app_dir)

        scaffold_app(context, app_dir)

        # First call is apx init, subsequent calls may be shadcn
        first_call = mock_run.call_args_list[0]
        cmd = first_call[0][0] if first_call[0] else first_call[1]["args"]

        assert cmd[0] == "apx"
        assert cmd[1] == "init"
        assert "--name" in cmd
        assert cmd[cmd.index("--name") + 1] == "acme-bank-fsi-app"
        assert "--addons" in cmd
        assert cmd[cmd.index("--addons") + 1] == "ui,sidebar,sql,claude"
        assert "--profile" in cmd
        assert cmd[cmd.index("--profile") + 1] == "DEFAULT"
        assert cmd[-1] == str(app_dir)

    @patch("exokit.core.apx_bridge.subprocess.run")
    def test_no_profile_flag_when_none(
        self, mock_run: MagicMock, context_no_profile: ScaffoldContext, tmp_path: Path
    ) -> None:
        """When databricks_profile is None, --profile should not appear."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=[], returncode=0, stdout="OK", stderr=""
        )
        app_dir = tmp_path / "app"
        _create_apx_layout(app_dir)

        scaffold_app(context_no_profile, app_dir)

        cmd = mock_run.call_args_list[0][0][0]
        assert "--profile" not in cmd

    @patch("exokit.core.apx_bridge.subprocess.run")
    def test_subprocess_called_with_correct_kwargs(
        self, mock_run: MagicMock, context: ScaffoldContext, tmp_path: Path
    ) -> None:
        """Verify capture_output, text, and timeout are set correctly."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=[], returncode=0, stdout="OK", stderr=""
        )
        app_dir = tmp_path / "app"
        _create_apx_layout(app_dir)

        scaffold_app(context, app_dir)

        # Check the first call (apx init)
        _, kwargs = mock_run.call_args_list[0]
        assert kwargs["capture_output"] is True
        assert kwargs["text"] is True
        assert kwargs["timeout"] == 120

    @patch("exokit.core.apx_bridge.subprocess.run")
    def test_governance_overlay_applied(
        self, mock_run: MagicMock, context: ScaffoldContext, tmp_path: Path
    ) -> None:
        """After scaffold, governance overlay creates 3-layer dirs and CLAUDE.md."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=[], returncode=0, stdout="OK", stderr=""
        )
        app_dir = tmp_path / "app"
        backend = _create_apx_layout(app_dir)

        scaffold_app(context, app_dir)

        # CLAUDE.md should exist in app_dir
        assert (app_dir / "CLAUDE.md").exists()

        # 3-layer directories should exist in the backend directory
        for layer in _BACKEND_LAYERS:
            assert (backend / layer).is_dir()
            assert (backend / layer / "__init__.py").exists()


# ---------------------------------------------------------------------------
# scaffold_app — error handling
# ---------------------------------------------------------------------------


class TestScaffoldAppErrors:
    """Tests for APX error handling."""

    @patch("exokit.core.apx_bridge.subprocess.run")
    def test_apx_not_installed(
        self, mock_run: MagicMock, context: ScaffoldContext, tmp_path: Path
    ) -> None:
        """FileNotFoundError from subprocess should raise APXNotInstalledError."""
        mock_run.side_effect = FileNotFoundError()

        with pytest.raises(APXNotInstalledError, match="APX CLI not found"):
            scaffold_app(context, tmp_path / "app")

    @patch("exokit.core.apx_bridge.subprocess.run")
    def test_apx_non_zero_exit(
        self, mock_run: MagicMock, context: ScaffoldContext, tmp_path: Path
    ) -> None:
        """Non-zero exit code should raise APXScaffoldError."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=[], returncode=1, stdout="", stderr="Error: invalid workspace"
        )

        with pytest.raises(APXScaffoldError, match="APX scaffold failed"):
            scaffold_app(context, tmp_path / "app")

    @patch("exokit.core.apx_bridge.subprocess.run")
    def test_apx_timeout(
        self, mock_run: MagicMock, context: ScaffoldContext, tmp_path: Path
    ) -> None:
        """TimeoutExpired should raise APXTimeoutError."""
        mock_run.side_effect = subprocess.TimeoutExpired(cmd="apx", timeout=120)

        with pytest.raises(APXTimeoutError, match="timed out"):
            scaffold_app(context, tmp_path / "app")

    @patch("exokit.core.apx_bridge.subprocess.run")
    def test_error_message_includes_stderr(
        self, mock_run: MagicMock, context: ScaffoldContext, tmp_path: Path
    ) -> None:
        """The error message should include stderr output from APX."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=[], returncode=2, stdout="", stderr="workspace auth failed"
        )

        with pytest.raises(APXScaffoldError, match="workspace auth failed"):
            scaffold_app(context, tmp_path / "app")


# ---------------------------------------------------------------------------
# Governance overlay — directory creation
# ---------------------------------------------------------------------------


class TestGovernanceOverlay:
    """Tests for the governance overlay applied after APX scaffold."""

    def test_raises_error_when_backend_missing(
        self, context: ScaffoldContext, tmp_path: Path
    ) -> None:
        """When no backend/ directory exists, APXScaffoldError is raised."""
        app_dir = tmp_path / "app"
        app_dir.mkdir()

        with pytest.raises(APXScaffoldError, match="expected backend directory"):
            _apply_governance_overlay(context, app_dir)

    def test_creates_backend_layers_in_apx_backend(
        self, context: ScaffoldContext, tmp_path: Path
    ) -> None:
        """Layers are created inside APX's src/{slug}/backend/ directory."""
        app_dir = tmp_path / "app"
        apx_backend = _create_apx_layout(app_dir)

        _apply_governance_overlay(context, app_dir)

        for layer in _BACKEND_LAYERS:
            assert (apx_backend / layer).is_dir()
            assert (apx_backend / layer / "__init__.py").exists()

    def test_init_py_content(self, context: ScaffoldContext, tmp_path: Path) -> None:
        """Each layer gets a minimal __init__.py."""
        app_dir = tmp_path / "app"
        apx_backend = _create_apx_layout(app_dir)

        _apply_governance_overlay(context, app_dir)

        for layer in _BACKEND_LAYERS:
            init_content = (apx_backend / layer / "__init__.py").read_text()
            assert layer in init_content

    def test_claude_md_written(self, context: ScaffoldContext, tmp_path: Path) -> None:
        """App-specific CLAUDE.md is written with the correct app name."""
        app_dir = tmp_path / "app"
        _create_apx_layout(app_dir)

        _apply_governance_overlay(context, app_dir)

        claude_md = (app_dir / "CLAUDE.md").read_text()
        assert "acme-bank-fsi-app" in claude_md
        assert "3-layer" in claude_md
        assert "api/" in claude_md
        assert "services/" in claude_md
        assert "repositories/" in claude_md

    def test_claude_md_matches_template(self, context: ScaffoldContext, tmp_path: Path) -> None:
        """The written CLAUDE.md should match the template with substitutions."""
        app_dir = tmp_path / "app"
        _create_apx_layout(app_dir)

        _apply_governance_overlay(context, app_dir)

        app_slug = context.app_name.replace("-", "_")
        expected = APP_CLAUDE_MD.format(app_name=context.app_name, app_slug=app_slug)
        actual = (app_dir / "CLAUDE.md").read_text()
        assert actual == expected

    def test_idempotent_overlay(self, context: ScaffoldContext, tmp_path: Path) -> None:
        """Running overlay twice should not fail (directories exist_ok)."""
        app_dir = tmp_path / "app"
        apx_backend = _create_apx_layout(app_dir)

        _apply_governance_overlay(context, app_dir)
        # Run again — should not raise
        _apply_governance_overlay(context, app_dir)

        for layer in _BACKEND_LAYERS:
            assert (apx_backend / layer).is_dir()


# ---------------------------------------------------------------------------
# APX config post-processing
# ---------------------------------------------------------------------------


class TestAPXConfigPostprocessing:
    """Tests for _postprocess_apx_config."""

    def test_removes_database_instances(self, context: ScaffoldContext, tmp_path: Path) -> None:
        """database_instances should be removed from the config."""
        app_dir = tmp_path / "app"
        app_dir.mkdir()
        config = {
            "resources": {
                "database_instances": {"my_db": {"name": "test"}},
                "apps": {},
            },
            "targets": {},
        }
        (app_dir / "databricks.yml").write_text(yaml.dump(config), encoding="utf-8")

        _postprocess_apx_config(context, app_dir)

        result = yaml.safe_load((app_dir / "databricks.yml").read_text())
        assert "database_instances" not in result["resources"]

    def test_aligns_profile_and_host(self, context: ScaffoldContext, tmp_path: Path) -> None:
        """Profile and host should be updated to context values."""
        app_dir = tmp_path / "app"
        app_dir.mkdir()
        config = {
            "resources": {},
            "targets": {
                "dev": {
                    "workspace": {
                        "profile": "OLD_PROFILE",
                        "host": "https://old-host.net",
                    }
                }
            },
        }
        (app_dir / "databricks.yml").write_text(yaml.dump(config), encoding="utf-8")

        _postprocess_apx_config(context, app_dir)

        result = yaml.safe_load((app_dir / "databricks.yml").read_text())
        workspace = result["targets"]["dev"]["workspace"]
        assert workspace["profile"] == "DEFAULT"
        assert workspace["host"] == "https://adb-1234567890.azuredatabricks.net"

    def test_handles_missing_config(self, context: ScaffoldContext, tmp_path: Path) -> None:
        """When databricks.yml doesn't exist, function should not crash."""
        app_dir = tmp_path / "app"
        app_dir.mkdir()

        # Should not raise
        _postprocess_apx_config(context, app_dir)

    def test_adds_sql_warehouse_resource(self, context: ScaffoldContext, tmp_path: Path) -> None:
        """sql-warehouse resource should be added when not present."""
        app_dir = tmp_path / "app"
        app_dir.mkdir()
        config = {
            "resources": {
                "apps": {
                    "my-app": {
                        "resources": [],
                    }
                }
            },
            "targets": {},
        }
        (app_dir / "databricks.yml").write_text(yaml.dump(config), encoding="utf-8")

        _postprocess_apx_config(context, app_dir)

        result = yaml.safe_load((app_dir / "databricks.yml").read_text())
        app_resources = result["resources"]["apps"]["my-app"]["resources"]
        assert len(app_resources) == 1
        assert app_resources[0]["name"] == "sql-warehouse"
        assert app_resources[0]["sql_warehouse"]["id"] == "abc123def456"


# ---------------------------------------------------------------------------
# App README
# ---------------------------------------------------------------------------


class TestAppReadme:
    """Tests for write_app_readme."""

    def test_writes_readme(self, context: ScaffoldContext, tmp_path: Path) -> None:
        """write_app_readme should create README.md with setup instructions."""
        app_dir = tmp_path / "app"
        app_dir.mkdir()

        write_app_readme(context, app_dir)

        readme_path = app_dir / "README.md"
        assert readme_path.exists()
        content = readme_path.read_text()
        assert "Setup" in content
        assert "Local Development" in content
        assert "Deployment" in content

    def test_readme_contains_app_name(self, context: ScaffoldContext, tmp_path: Path) -> None:
        """README should contain the app name."""
        app_dir = tmp_path / "app"
        app_dir.mkdir()

        write_app_readme(context, app_dir)

        content = (app_dir / "README.md").read_text()
        assert "acme-bank-fsi-app" in content


# ---------------------------------------------------------------------------
# App .env post-processing
# ---------------------------------------------------------------------------


class TestAppEnvPostprocessing:
    """Tests for _postprocess_app_env."""

    def test_appends_missing_vars(self, context: ScaffoldContext, tmp_path: Path) -> None:
        """New vars should be appended to the .env file."""
        app_dir = tmp_path / "app"
        app_dir.mkdir()
        env_path = app_dir / ".env"
        env_path.write_text("EXISTING_VAR=value\n", encoding="utf-8")

        _postprocess_app_env(context, app_dir)

        content = env_path.read_text()
        assert "DATABRICKS_SQL_WAREHOUSE_ID=abc123def456" in content
        assert "CATALOG=acme_bank_fsi" in content
        assert "DATABRICKS_CONFIG_PROFILE=DEFAULT" in content
        assert "EXISTING_VAR=value" in content

    def test_does_not_duplicate_existing_vars(
        self, context: ScaffoldContext, tmp_path: Path
    ) -> None:
        """Existing vars should not be duplicated."""
        app_dir = tmp_path / "app"
        app_dir.mkdir()
        env_path = app_dir / ".env"
        env_path.write_text("CATALOG=already_set\n", encoding="utf-8")

        _postprocess_app_env(context, app_dir)

        content = env_path.read_text()
        # CATALOG should appear only once (the original)
        assert content.count("CATALOG=") == 1
        assert "CATALOG=already_set" in content

    def test_creates_env_if_missing(self, context: ScaffoldContext, tmp_path: Path) -> None:
        """If .env doesn't exist, it should be created with all vars."""
        app_dir = tmp_path / "app"
        app_dir.mkdir()

        _postprocess_app_env(context, app_dir)

        env_path = app_dir / ".env"
        assert env_path.exists()
        content = env_path.read_text()
        assert "DATABRICKS_SQL_WAREHOUSE_ID=abc123def456" in content
        assert "CATALOG=acme_bank_fsi" in content
