"""Tests for first-run onboarding flow.

Tests the state machine: key generation -> channel creation -> classification
defaults -> hub registration -> completion marker.
"""
import os
from unittest.mock import patch, MagicMock

import pytest

from greymatter_core.db import SQLiteDatabase
from greymatter_core.schema.solo import get_sqlite_ddl as get_solo_ddl
from greymatter_core.onboarding import (
    CLASSIFICATION_LEVELS,
    DEFAULT_CLASSIFICATION_KEY,
    ONBOARDING_STATE_KEY,
    PERSONAL_CHANNEL_ID,
    OnboardingResult,
    check_onboarding_complete,
    create_personal_channel,
    generate_master_key,
    is_first_run,
    mark_onboarding_complete,
    register_with_hub,
    run_onboarding,
    set_classification_defaults,
)


@pytest.fixture
def db():
    """In-memory SQLite database with core + solo schema."""
    d = SQLiteDatabase(":memory:")
    # Apply solo schema (not included in core migrate)
    d._conn.executescript(get_solo_ddl())
    return d


@pytest.fixture
def tmp_data_dir(tmp_path):
    """Temporary data directory for key file tests."""
    return tmp_path / ".greymatter"


# =========================================================================
# Classification levels
# =========================================================================

class TestClassificationLevels:
    def test_levels_defined(self):
        assert CLASSIFICATION_LEVELS == ("personal", "team", "public")

    def test_personal_is_most_private(self):
        assert CLASSIFICATION_LEVELS[0] == "personal"


# =========================================================================
# is_first_run
# =========================================================================

class TestIsFirstRun:
    def test_true_when_no_data_dir(self, tmp_path):
        assert is_first_run(tmp_path / "nonexistent") is True

    def test_true_when_no_keyfile(self, tmp_path):
        data_dir = tmp_path / ".greymatter"
        data_dir.mkdir()
        (data_dir / "greymatter.db").touch()
        assert is_first_run(data_dir) is True

    def test_true_when_no_db(self, tmp_path):
        data_dir = tmp_path / ".greymatter"
        data_dir.mkdir()
        (data_dir / ".keyfile").write_text("abcd1234")
        assert is_first_run(data_dir) is True

    def test_false_when_both_exist(self, tmp_path):
        data_dir = tmp_path / ".greymatter"
        data_dir.mkdir()
        (data_dir / ".keyfile").write_text("abcd1234")
        (data_dir / "greymatter.db").touch()
        assert is_first_run(data_dir) is False


# =========================================================================
# generate_master_key
# =========================================================================

class TestGenerateMasterKey:
    def test_generates_new_key(self, tmp_data_dir):
        key, was_generated = generate_master_key(tmp_data_dir)
        assert was_generated is True
        assert len(key) == 64  # 32 bytes = 64 hex chars
        keyfile = tmp_data_dir / ".keyfile"
        assert keyfile.exists()
        assert keyfile.read_text().strip() == key

    def test_loads_existing_key(self, tmp_data_dir):
        tmp_data_dir.mkdir(parents=True)
        existing_key = os.urandom(32).hex()
        (tmp_data_dir / ".keyfile").write_text(existing_key)

        key, was_generated = generate_master_key(tmp_data_dir)
        assert was_generated is False
        assert key == existing_key

    def test_regenerates_if_keyfile_empty(self, tmp_data_dir):
        tmp_data_dir.mkdir(parents=True)
        (tmp_data_dir / ".keyfile").write_text("")

        key, was_generated = generate_master_key(tmp_data_dir)
        assert was_generated is True
        assert len(key) == 64

    def test_key_is_valid_hex(self, tmp_data_dir):
        key, _ = generate_master_key(tmp_data_dir)
        int(key, 16)

    def test_creates_data_dir_if_missing(self, tmp_data_dir):
        assert not tmp_data_dir.exists()
        generate_master_key(tmp_data_dir)
        assert tmp_data_dir.exists()


# =========================================================================
# create_personal_channel
# =========================================================================

class TestCreatePersonalChannel:
    def test_creates_channel(self, db):
        created = create_personal_channel(db)
        assert created is True

        channel = db.execute_one(
            "SELECT * FROM solo_channels WHERE id = ?",
            (PERSONAL_CHANNEL_ID,),
        )
        assert channel is not None
        assert channel["name"] == "Personal"
        assert channel["classification"] == "personal"
        assert channel["encrypted"] == 1
        assert channel["sync_enabled"] == 0

    def test_idempotent(self, db):
        assert create_personal_channel(db) is True
        assert create_personal_channel(db) is False

        channels = db.execute("SELECT * FROM solo_channels")
        assert len(channels) == 1

    def test_channel_never_syncs(self, db):
        create_personal_channel(db)
        channel = db.execute_one(
            "SELECT sync_enabled FROM solo_channels WHERE id = ?",
            (PERSONAL_CHANNEL_ID,),
        )
        assert channel["sync_enabled"] == 0


# =========================================================================
# set_classification_defaults
# =========================================================================

class TestSetClassificationDefaults:
    def test_sets_default_to_personal(self, db):
        result = set_classification_defaults(db)
        assert result is True

        row = db.execute_one(
            "SELECT value FROM solo_config WHERE key = ?",
            (DEFAULT_CLASSIFICATION_KEY,),
        )
        assert row["value"] == "personal"

    def test_idempotent(self, db):
        assert set_classification_defaults(db) is True
        assert set_classification_defaults(db) is False

    def test_does_not_overwrite_existing(self, db):
        db.execute(
            "INSERT INTO solo_config (key, value) VALUES (?, ?)",
            (DEFAULT_CLASSIFICATION_KEY, "team"),
        )
        result = set_classification_defaults(db)
        assert result is False

        row = db.execute_one(
            "SELECT value FROM solo_config WHERE key = ?",
            (DEFAULT_CLASSIFICATION_KEY,),
        )
        assert row["value"] == "team"


# =========================================================================
# register_with_hub
# =========================================================================

class TestRegisterWithHub:
    def test_successful_registration(self, db):
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "team_id": "team-abc",
            "team_name": "Test Team",
        }
        mock_response.raise_for_status = MagicMock()

        with patch("greymatter_core.onboarding.httpx.post", return_value=mock_response):
            success, error = register_with_hub(
                db,
                hub_url="http://localhost:8200",
                node_id="test-node",
                node_name="Test Node",
            )

        assert success is True
        assert error == ""

        membership = db.execute_one(
            "SELECT * FROM solo_teams_memberships WHERE cluster_url = ?",
            ("http://localhost:8200",),
        )
        assert membership is not None
        assert membership["team_id"] == "team-abc"
        assert membership["team_name"] == "Test Team"
        assert membership["status"] == "registered"

    def test_skips_if_already_registered(self, db):
        db.execute(
            """INSERT INTO solo_teams_memberships
               (team_id, team_name, cluster_url, status, joined_at)
               VALUES (?, ?, ?, ?, datetime('now'))""",
            ("team-abc", "Test Team", "http://localhost:8200", "registered"),
        )

        success, error = register_with_hub(
            db,
            hub_url="http://localhost:8200",
            node_id="test-node",
        )
        assert success is True
        assert error == ""

    def test_handles_http_error(self, db):
        import httpx

        with patch(
            "greymatter_core.onboarding.httpx.post",
            side_effect=httpx.ConnectError("Connection refused"),
        ):
            success, error = register_with_hub(
                db,
                hub_url="http://unreachable:8200",
                node_id="test-node",
            )

        assert success is False
        assert "Connection refused" in error

    def test_strips_trailing_slash(self, db):
        mock_response = MagicMock()
        mock_response.json.return_value = {"team_id": "t1", "team_name": "T"}
        mock_response.raise_for_status = MagicMock()

        with patch("greymatter_core.onboarding.httpx.post", return_value=mock_response) as mock_post:
            register_with_hub(db, hub_url="http://hub:8200/", node_id="n1")

        call_url = mock_post.call_args[0][0]
        assert call_url == "http://hub:8200/api/teams/register"


# =========================================================================
# mark_onboarding_complete / check_onboarding_complete
# =========================================================================

class TestOnboardingCompletionMarker:
    def test_mark_and_check(self, db):
        assert check_onboarding_complete(db) is False
        mark_onboarding_complete(db)
        assert check_onboarding_complete(db) is True

    def test_mark_is_idempotent(self, db):
        mark_onboarding_complete(db)
        mark_onboarding_complete(db)
        assert check_onboarding_complete(db) is True


# =========================================================================
# run_onboarding (full flow)
# =========================================================================

class TestRunOnboarding:
    def test_full_flow_without_hub(self, db, tmp_data_dir):
        result = run_onboarding(db=db, data_dir=tmp_data_dir)

        assert result.completed is True
        assert result.master_key_generated is True
        assert result.personal_channel_created is True
        assert result.classification_set is True
        assert result.hub_registered is False
        assert "master_key" in result.steps_completed
        assert "personal_channel" in result.steps_completed
        assert "classification_defaults" in result.steps_completed
        assert "complete" in result.steps_completed

        assert check_onboarding_complete(db) is True

    def test_full_flow_with_hub(self, db, tmp_data_dir):
        mock_response = MagicMock()
        mock_response.json.return_value = {"team_id": "t1", "team_name": "T"}
        mock_response.raise_for_status = MagicMock()

        with patch("greymatter_core.onboarding.httpx.post", return_value=mock_response):
            result = run_onboarding(
                db=db,
                data_dir=tmp_data_dir,
                hub_url="http://hub:8200",
                node_id="test-node",
            )

        assert result.completed is True
        assert result.hub_registered is True
        assert "hub_registration" in result.steps_completed

    def test_skips_if_already_completed(self, db, tmp_data_dir):
        run_onboarding(db=db, data_dir=tmp_data_dir)

        result = run_onboarding(db=db, data_dir=tmp_data_dir)
        assert result.completed is True
        assert result.master_key_generated is False
        assert result.personal_channel_created is False
        assert result.steps_completed == []

    def test_force_reruns(self, db, tmp_data_dir):
        run_onboarding(db=db, data_dir=tmp_data_dir)

        result = run_onboarding(db=db, data_dir=tmp_data_dir, force=True)
        assert result.completed is True
        assert result.master_key_existed is True
        assert result.personal_channel_existed is True
        assert len(result.steps_completed) > 0

    def test_hub_failure_does_not_block_completion(self, db, tmp_data_dir):
        import httpx

        with patch(
            "greymatter_core.onboarding.httpx.post",
            side_effect=httpx.ConnectError("refused"),
        ):
            result = run_onboarding(
                db=db,
                data_dir=tmp_data_dir,
                hub_url="http://unreachable:8200",
            )

        assert result.completed is True
        assert result.hub_registered is False
        assert "refused" in result.hub_registration_error
        assert result.master_key_generated is True
        assert result.personal_channel_created is True
        assert result.classification_set is True

    def test_result_dataclass_defaults(self):
        result = OnboardingResult()
        assert result.completed is False
        assert result.master_key_generated is False
        assert result.hub_registration_error == ""
        assert result.steps_completed == []
