"""Unit tests for Novyx CLI."""
import json
import tempfile
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock

import pytest
from click.testing import CliRunner

from novyx.cli import (
    cli,
    load_config,
    save_config,
    get_api_key,
    CONFIG_FILE,
)
# Import rollback group separately to avoid name conflict
from novyx import cli as cli_module


@pytest.fixture
def runner():
    """Create CLI test runner."""
    return CliRunner()


@pytest.fixture
def temp_config():
    """Create temporary config file."""
    with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.json') as f:
        config = {"api_key": "nram_test_12345678"}
        json.dump(config, f)
        temp_path = Path(f.name)

    yield temp_path

    # Cleanup
    if temp_path.exists():
        temp_path.unlink()


@pytest.fixture
def mock_client():
    """Create mock Novyx client."""
    client = Mock()

    # Mock methods
    client.memories.return_value = [
        {
            "uuid": "test-uuid-123",
            "observation": "Test memory",
            "importance": 5,
            "tags": ["test"],
            "created_at": "2026-02-08T10:00:00Z",
        }
    ]

    client.recall.return_value = [
        {
            "uuid": "test-uuid-456",
            "observation": "Test search result",
            "importance": 7,
            "score": 0.95,
        }
    ]

    client.stats.return_value = {"total_memories": 42}

    client.forget.return_value = True

    client.rollback_preview.return_value = {
        "target_timestamp": "2026-02-08T08:00:00Z",
        "artifacts_modified": 5,
        "artifacts_deleted": 2,
        "safe_rollback": True,
        "warnings": [],
    }

    client.rollback.return_value = {
        "success": True,
        "rolled_back_to": "2026-02-08T08:00:00Z",
        "artifacts_restored": 5,
        "operations_undone": 10,
    }

    client.rollback_history.return_value = [
        {
            "executed_at": "2026-02-08T10:00:00Z",
            "target_timestamp": "2026-02-08T08:00:00Z",
            "artifacts_restored": 5,
            "operations_undone": 10,
        }
    ]

    client.audit.return_value = [
        {
            "timestamp": "2026-02-08T10:00:00Z",
            "operation": "CREATE",
            "artifact_id": "test-artifact-123",
            "agent_id": "test-agent",
            "content_hash": "sha256:abc123",
        }
    ]

    client.audit_export.return_value = b"timestamp,operation\n2026-02-08,CREATE"

    client.audit_verify.return_value = {
        "valid": True,
        "errors": [],
        "total_entries": 100,
        "chain_head": "abc123def456",
    }

    client.trace_verify.return_value = {
        "valid": True,
        "errors": [],
        "steps_verified": 10,
    }

    client.health.return_value = {
        "status": "healthy",
        "version": "2.0.0",
    }

    client.usage.return_value = {
        "tier": "pro",
        "api_calls": {"current": 100, "limit": 100000, "unlimited": False},
        "rollbacks": {"current": 5, "limit": None, "unlimited": True},
        "memories": {"current": 50, "limit": None, "unlimited": True},
        "audit_retention_days": 30,
        "features": {
            "anomaly_alerts": True,
            "trace_audit": True,
            "priority_support": False,
            "sso_ready": False,
        },
    }

    return client


# =============================================================================
# Config Tests
# =============================================================================

def test_config_set(runner):
    """Test setting configuration."""
    with runner.isolated_filesystem():
        result = runner.invoke(cli, ["config", "set", "api_key", "nram_test_key"])
        assert result.exit_code == 0
        assert "Configuration saved" in result.output


def test_config_show(runner, temp_config):
    """Test showing configuration."""
    with patch("novyx.cli.CONFIG_FILE", temp_config):
        result = runner.invoke(cli, ["config", "show"])
        assert result.exit_code == 0
        # API key should be masked
        assert "nram_test_1..." in result.output or "api_key" in result.output


def test_config_reset(runner):
    """Test resetting configuration."""
    with runner.isolated_filesystem():
        # Create config first
        runner.invoke(cli, ["config", "set", "api_key", "test"])

        # Reset with --yes flag
        result = runner.invoke(cli, ["config", "reset", "--yes"])
        assert result.exit_code == 0
        assert "Configuration reset" in result.output or "No configuration" in result.output


# =============================================================================
# Memory Tests
# =============================================================================

@patch("novyx.cli.Novyx")
def test_memories_list(mock_novyx_class, runner, mock_client):
    """Test listing memories."""
    mock_novyx_class.return_value = mock_client

    result = runner.invoke(cli, [
        "--api-key", "nram_test_key",
        "memories", "list",
        "--limit", "10",
        "--format", "json"
    ])

    assert result.exit_code == 0
    assert "test-uuid-123" in result.output or "Test memory" in result.output


@patch("novyx.cli.Novyx")
def test_memories_search(mock_novyx_class, runner, mock_client):
    """Test searching memories."""
    mock_novyx_class.return_value = mock_client

    result = runner.invoke(cli, [
        "--api-key", "nram_test_key",
        "memories", "search", "test query",
        "--format", "json"
    ])

    assert result.exit_code == 0
    assert "test-uuid-456" in result.output or "Test search" in result.output


@patch("novyx.cli.Novyx")
def test_memories_count(mock_novyx_class, runner, mock_client):
    """Test getting memory count."""
    mock_novyx_class.return_value = mock_client

    result = runner.invoke(cli, [
        "--api-key", "nram_test_key",
        "memories", "count"
    ])

    assert result.exit_code == 0
    assert "42" in result.output


@patch("novyx.cli.Novyx")
def test_memories_delete(mock_novyx_class, runner, mock_client):
    """Test deleting memory."""
    mock_novyx_class.return_value = mock_client

    result = runner.invoke(cli, [
        "--api-key", "nram_test_key",
        "memories", "delete", "test-uuid-123",
        "--yes"  # Skip confirmation
    ])

    assert result.exit_code == 0
    assert "deleted successfully" in result.output or "Memory" in result.output


# =============================================================================
# Rollback Tests
# =============================================================================

@patch("novyx.cli.Novyx")
def test_rollback_preview(mock_novyx_class, runner, mock_client):
    """Test rollback preview."""
    mock_novyx_class.return_value = mock_client

    # Import rollback group to test subcommands directly
    from novyx.cli import rollback as rollback_group

    # Create a context with api_key for the rollback group
    with runner.isolated_filesystem():
        result = runner.invoke(rollback_group, [
            "preview", "2026-02-08T08:00:00Z"
        ], obj={"api_key": "nram_test_key"})

    assert result.exit_code == 0
    assert "Preview" in result.output or "Artifacts Modified" in result.output or "5" in result.output


@patch("novyx.cli.Novyx")
def test_rollback_execute_with_confirmation(mock_novyx_class, runner, mock_client):
    """Test rollback execution with --yes flag."""
    mock_novyx_class.return_value = mock_client

    # Import rollback group to test subcommands directly
    from novyx.cli import rollback as rollback_group

    with runner.isolated_filesystem():
        result = runner.invoke(rollback_group, [
            "execute", "2026-02-08T08:00:00Z",
            "--yes"  # Skip confirmation
        ], obj={"api_key": "nram_test_key"})

    assert result.exit_code == 0
    assert "success" in result.output.lower() or "Rolled back" in result.output


@patch("novyx.cli.Novyx")
def test_rollback_history(mock_novyx_class, runner, mock_client):
    """Test rollback history."""
    mock_novyx_class.return_value = mock_client

    # Import rollback group to test subcommands directly
    from novyx.cli import rollback as rollback_group

    with runner.isolated_filesystem():
        result = runner.invoke(rollback_group, [
            "history"
        ], obj={"api_key": "nram_test_key"})

    assert result.exit_code == 0
    # Should show timestamp or rollback info
    assert "2026" in result.output or "Rollback" in result.output


@patch("novyx.cli.Novyx")
def test_rollback_shortcut(mock_novyx_class, runner, mock_client):
    """Test rollback shortcut command."""
    mock_novyx_class.return_value = mock_client

    result = runner.invoke(cli, [
        "--api-key", "nram_test_key",
        "rollback", "2026-02-08T09:00:00Z",
        "--yes"
    ])

    assert result.exit_code == 0


# =============================================================================
# Audit Tests
# =============================================================================

@patch("novyx.cli.Novyx")
def test_audit_list(mock_novyx_class, runner, mock_client):
    """Test listing audit entries."""
    mock_novyx_class.return_value = mock_client

    result = runner.invoke(cli, [
        "--api-key", "nram_test_key",
        "audit", "list",
        "--limit", "10",
        "--format", "json"
    ])

    assert result.exit_code == 0
    assert "CREATE" in result.output or "test-artifact" in result.output


@patch("novyx.cli.Novyx")
def test_audit_export(mock_novyx_class, runner, mock_client):
    """Test exporting audit log."""
    mock_novyx_class.return_value = mock_client

    result = runner.invoke(cli, [
        "--api-key", "nram_test_key",
        "audit", "export",
        "--format", "csv"
    ])

    assert result.exit_code == 0
    # Should output CSV data
    assert b"timestamp" in result.stdout_bytes or b"CREATE" in result.stdout_bytes


@patch("novyx.cli.Novyx")
def test_audit_verify_valid(mock_novyx_class, runner, mock_client):
    """Test audit verification (valid)."""
    mock_novyx_class.return_value = mock_client

    result = runner.invoke(cli, [
        "--api-key", "nram_test_key",
        "audit", "verify"
    ])

    assert result.exit_code == 0
    assert "verified" in result.output.lower() or "valid" in result.output.lower()


@patch("novyx.cli.Novyx")
def test_audit_verify_invalid(mock_novyx_class, runner, mock_client):
    """Test audit verification (invalid)."""
    mock_client.audit_verify.return_value = {
        "valid": False,
        "errors": ["Hash mismatch at entry 50"],
        "total_entries": 100,
    }
    mock_novyx_class.return_value = mock_client

    result = runner.invoke(cli, [
        "--api-key", "nram_test_key",
        "audit", "verify"
    ])

    assert result.exit_code == 1  # Should fail with exit code 1
    assert "failed" in result.output.lower() or "error" in result.output.lower()


# =============================================================================
# Trace Tests
# =============================================================================

@patch("novyx.cli.Novyx")
def test_traces_verify(mock_novyx_class, runner, mock_client):
    """Test trace verification."""
    mock_novyx_class.return_value = mock_client

    result = runner.invoke(cli, [
        "--api-key", "nram_test_key",
        "traces", "verify", "trace-123"
    ])

    assert result.exit_code == 0
    assert "verified" in result.output.lower() or "valid" in result.output.lower()


# =============================================================================
# Status Tests
# =============================================================================

@patch("novyx.cli.Novyx")
def test_status(mock_novyx_class, runner, mock_client):
    """Test status command."""
    mock_novyx_class.return_value = mock_client

    result = runner.invoke(cli, [
        "--api-key", "nram_test_key",
        "status"
    ])

    assert result.exit_code == 0
    assert "healthy" in result.output.lower() or "pro" in result.output.lower()


# =============================================================================
# Error Handling Tests
# =============================================================================

@patch("novyx.cli.Novyx")
def test_invalid_api_key(mock_novyx_class, runner):
    """Test handling of invalid API key."""
    from novyx import NovyxAuthError

    mock_client = Mock()
    mock_client.memories.side_effect = NovyxAuthError("Invalid API key")
    mock_novyx_class.return_value = mock_client

    result = runner.invoke(cli, [
        "--api-key", "invalid_key",
        "memories", "list"
    ])

    assert result.exit_code == 1
    assert "Invalid API key" in result.output or "Error" in result.output


@patch("novyx.cli.Novyx")
def test_forbidden_error(mock_novyx_class, runner):
    """Test handling of forbidden error (Pro+ feature on Free tier)."""
    from novyx import NovyxForbiddenError

    mock_client = Mock()
    mock_client.rollback_preview.side_effect = NovyxForbiddenError(
        "Rollback requires Pro+ tier",
        data={"feature": "rollback", "tier_required": "pro"}
    )
    mock_novyx_class.return_value = mock_client

    # Import rollback group to test subcommands directly
    from novyx.cli import rollback as rollback_group

    with runner.isolated_filesystem():
        result = runner.invoke(rollback_group, [
            "preview", "2026-02-08T08:00:00Z"
        ], obj={"api_key": "nram_test_key"})

    assert result.exit_code == 1
    assert "Pro+" in result.output or "tier" in result.output.lower()


@patch("novyx.cli.Novyx")
def test_rate_limit_error(mock_novyx_class, runner):
    """Test handling of rate limit error."""
    from novyx import NovyxRateLimitError

    mock_client = Mock()
    mock_client.rollback_preview.return_value = {
        "target_timestamp": "2026-02-08T08:00:00Z",
        "artifacts_modified": 5,
        "artifacts_deleted": 2,
        "safe_rollback": True,
        "warnings": [],
    }
    mock_client.rollback.side_effect = NovyxRateLimitError(
        "Rollback limit exceeded",
        data={"limit": "rollbacks", "current": 3, "max": 3, "tier": "free"}
    )
    mock_novyx_class.return_value = mock_client

    # Import rollback group to test subcommands directly
    from novyx.cli import rollback as rollback_group

    with runner.isolated_filesystem():
        result = runner.invoke(rollback_group, [
            "execute", "2026-02-08T08:00:00Z",
            "--yes"
        ], obj={"api_key": "nram_test_key"})

    assert result.exit_code == 1
    assert "limit" in result.output.lower() or "exceeded" in result.output.lower()


# =============================================================================
# API Key Resolution Tests
# =============================================================================

def test_api_key_from_flag(runner):
    """Test API key from --api-key flag."""
    with runner.isolated_filesystem():
        key = get_api_key("nram_flag_key")
        assert key == "nram_flag_key"


def test_api_key_from_env(runner, monkeypatch):
    """Test API key from environment variable."""
    monkeypatch.setenv("NOVYX_API_KEY", "nram_env_key")

    key = get_api_key(None)
    assert key == "nram_env_key"


def test_api_key_from_config(runner, temp_config):
    """Test API key from config file."""
    with patch("novyx.cli.CONFIG_FILE", temp_config):
        key = get_api_key(None)
        assert key == "nram_test_12345678"


def test_api_key_priority_order(runner, temp_config, monkeypatch):
    """Test API key priority: flag > env > config."""
    monkeypatch.setenv("NOVYX_API_KEY", "nram_env_key")

    with patch("novyx.cli.CONFIG_FILE", temp_config):
        # Flag takes priority
        key = get_api_key("nram_flag_key")
        assert key == "nram_flag_key"

        # Env takes priority over config
        key = get_api_key(None)
        assert key == "nram_env_key"


def test_no_api_key_exits(runner, capsys):
    """Test that missing API key causes exit."""
    with pytest.raises(SystemExit) as exc_info:
        get_api_key(None)

    assert exc_info.value.code == 1


# =============================================================================
# Integration Tests
# =============================================================================

@patch("novyx.cli.Novyx")
def test_full_workflow(mock_novyx_class, runner, mock_client):
    """Test full CLI workflow: status -> list -> search -> rollback."""
    mock_novyx_class.return_value = mock_client

    # 1. Check status
    result = runner.invoke(cli, ["--api-key", "nram_test", "status"])
    assert result.exit_code == 0

    # 2. List memories
    result = runner.invoke(cli, [
        "--api-key", "nram_test",
        "memories", "list",
        "--limit", "10"
    ])
    assert result.exit_code == 0

    # 3. Search
    result = runner.invoke(cli, [
        "--api-key", "nram_test",
        "memories", "search", "test"
    ])
    assert result.exit_code == 0

    # 4. Rollback preview using rollback group
    from novyx.cli import rollback as rollback_group

    with runner.isolated_filesystem():
        result = runner.invoke(rollback_group, [
            "preview", "2026-02-08T09:00:00Z"
        ], obj={"api_key": "nram_test"})
    assert result.exit_code == 0


@patch("novyx.cli.Novyx")
def test_json_output_format(mock_novyx_class, runner, mock_client):
    """Test JSON output format."""
    mock_novyx_class.return_value = mock_client

    result = runner.invoke(cli, [
        "--api-key", "nram_test",
        "memories", "list",
        "--format", "json"
    ])

    assert result.exit_code == 0

    # Should be valid JSON
    try:
        data = json.loads(result.output)
        assert isinstance(data, list)
    except json.JSONDecodeError:
        pytest.fail("Output is not valid JSON")


@patch("novyx.cli.Novyx")
def test_table_output_format(mock_novyx_class, runner, mock_client):
    """Test table output format."""
    mock_novyx_class.return_value = mock_client

    result = runner.invoke(cli, [
        "--api-key", "nram_test",
        "memories", "list",
        "--format", "table"
    ])

    assert result.exit_code == 0
    # Table should have some structure (headers, data)
    assert result.output  # Non-empty output
