"""
Tests for Phase 50: External Service Integration Hub.
"""

import json
import time
import pytest


@pytest.fixture(autouse=True)
def _isolate(tmp_path, monkeypatch):
    """Redirect all integration hub storage to tmp_path and reset handlers."""
    integrations_dir = tmp_path / "integrations"
    integrations_dir.mkdir(parents=True, exist_ok=True)

    import brynq.server as srv
    monkeypatch.setattr(srv, "BROKER_DIR", tmp_path)

    import brynq.integration_hub as ih
    monkeypatch.setattr(ih, "INTEGRATIONS_DIR", integrations_dir)
    monkeypatch.setattr(ih, "EVENTS_FILE", integrations_dir / "events.jsonl")

    ih._reset_handlers()


# ── Registration ──────────────────────────────────────────────────────────


class TestRegisterIntegration:
    def test_register_basic(self):
        from brynq.integration_hub import register_integration
        result = register_integration("my-slack", "slack", {"webhook_url": "https://hooks.slack.com/x"})
        assert result["name"] == "my-slack"
        assert result["service_type"] == "slack"
        assert result["status"] == "active"
        assert result["stats"]["events_sent"] == 0

    def test_register_with_description(self):
        from brynq.integration_hub import register_integration
        result = register_integration("gh", "github", {}, description="GitHub CI")
        assert result["description"] == "GitHub CI"

    def test_register_default_description(self):
        from brynq.integration_hub import register_integration
        result = register_integration("gh", "github", {})
        assert "github" in result["description"]

    def test_register_invalid_service_type(self):
        from brynq.integration_hub import register_integration
        result = register_integration("x", "foobar", {})
        assert "error" in result

    def test_register_empty_name(self):
        from brynq.integration_hub import register_integration
        result = register_integration("", "slack", {})
        assert "error" in result

    def test_register_duplicate(self):
        from brynq.integration_hub import register_integration
        register_integration("dup", "slack", {})
        result = register_integration("dup", "slack", {})
        assert "error" in result

    def test_register_all_service_types(self):
        from brynq.integration_hub import register_integration, VALID_SERVICE_TYPES
        for st in VALID_SERVICE_TYPES:
            result = register_integration(f"test-{st}", st, {})
            assert result["service_type"] == st


# ── List / Get ────────────────────────────────────────────────────────────


class TestListGet:
    def test_get_existing(self):
        from brynq.integration_hub import register_integration, get_integration
        register_integration("myint", "github", {"api_url": "https://api.github.com"})
        result = get_integration("myint")
        assert result is not None
        assert result["name"] == "myint"

    def test_get_nonexistent(self):
        from brynq.integration_hub import get_integration
        assert get_integration("nope") is None

    def test_list_all(self):
        from brynq.integration_hub import register_integration, list_integrations
        register_integration("a", "slack", {})
        register_integration("b", "github", {})
        result = list_integrations()
        assert len(result) == 2

    def test_list_filter_service_type(self):
        from brynq.integration_hub import register_integration, list_integrations
        register_integration("s1", "slack", {})
        register_integration("g1", "github", {})
        result = list_integrations(service_type="slack")
        assert len(result) == 1
        assert result[0]["service_type"] == "slack"

    def test_list_filter_status(self):
        from brynq.integration_hub import register_integration, disable_integration, list_integrations
        register_integration("a1", "slack", {})
        register_integration("b1", "github", {})
        disable_integration("b1")
        active = list_integrations(status="active")
        paused = list_integrations(status="paused")
        assert len(active) == 1
        assert len(paused) == 1

    def test_list_empty(self):
        from brynq.integration_hub import list_integrations
        assert list_integrations() == []


# ── Enable / Disable / Remove ─────────────────────────────────────────────


class TestLifecycle:
    def test_disable(self):
        from brynq.integration_hub import register_integration, disable_integration, get_integration
        register_integration("d1", "slack", {})
        disable_integration("d1")
        rec = get_integration("d1")
        assert rec["status"] == "paused"

    def test_enable_after_disable(self):
        from brynq.integration_hub import register_integration, disable_integration, enable_integration, get_integration
        register_integration("e1", "slack", {})
        disable_integration("e1")
        enable_integration("e1")
        rec = get_integration("e1")
        assert rec["status"] == "active"

    def test_remove(self):
        from brynq.integration_hub import register_integration, remove_integration, get_integration
        register_integration("r1", "slack", {})
        result = remove_integration("r1")
        assert result == {"removed": "r1"}
        assert get_integration("r1") is None

    def test_remove_nonexistent(self):
        from brynq.integration_hub import remove_integration
        result = remove_integration("nope")
        assert "error" in result

    def test_disable_nonexistent(self):
        from brynq.integration_hub import disable_integration
        result = disable_integration("nope")
        assert "error" in result

    def test_enable_nonexistent(self):
        from brynq.integration_hub import enable_integration
        result = enable_integration("nope")
        assert "error" in result

    def test_remove_cleans_handlers(self):
        from brynq.integration_hub import (
            register_integration, register_incoming_handler,
            remove_integration, _incoming_handlers,
        )
        register_integration("h1", "slack", {})
        register_incoming_handler("h1", "msg", lambda et, p: None)
        assert any(k[0] == "h1" for k in _incoming_handlers)
        remove_integration("h1")
        assert not any(k[0] == "h1" for k in _incoming_handlers)


# ── Send Event (outbound) ────────────────────────────────────────────────


class TestSendEvent:
    def test_send_not_found(self):
        from brynq.integration_hub import send_to_integration
        ok, msg = send_to_integration("nope", "test", {})
        assert ok is False
        assert "not found" in msg

    def test_send_paused(self):
        from brynq.integration_hub import register_integration, disable_integration, send_to_integration
        register_integration("p1", "slack", {"webhook_url": "https://example.com"})
        disable_integration("p1")
        ok, msg = send_to_integration("p1", "test", {})
        assert ok is False
        assert "paused" in msg

    def test_send_no_url(self):
        from brynq.integration_hub import register_integration, send_to_integration, get_integration
        register_integration("no-url", "slack", {})
        ok, msg = send_to_integration("no-url", "test", {})
        assert ok is False
        assert "No URL" in msg
        # Should set status to error
        rec = get_integration("no-url")
        assert rec["status"] == "error"

    def test_send_with_mock_http(self, monkeypatch):
        import brynq.integration_hub as ih
        from brynq.integration_hub import register_integration, send_to_integration, get_integration

        register_integration("mock-wh", "custom_webhook", {"webhook_url": "https://example.com/hook"})

        monkeypatch.setattr(ih, "_do_http_post", lambda url, payload, headers=None, timeout=10: (True, '{"ok":true}'))

        ok, resp = send_to_integration("mock-wh", "task.created", {"task_id": "t1"})
        assert ok is True
        assert "ok" in resp

        rec = get_integration("mock-wh")
        assert rec["stats"]["events_sent"] == 1
        assert rec["stats"]["last_event_at"] is not None

    def test_send_records_error_on_failure(self, monkeypatch):
        import brynq.integration_hub as ih
        from brynq.integration_hub import register_integration, send_to_integration, get_integration

        register_integration("fail-wh", "custom_webhook", {"webhook_url": "https://example.com/hook"})
        monkeypatch.setattr(ih, "_do_http_post", lambda url, payload, headers=None, timeout=10: (False, "timeout"))

        ok, resp = send_to_integration("fail-wh", "ping", {})
        assert ok is False
        rec = get_integration("fail-wh")
        assert rec["stats"]["errors"] >= 1


# ── Payload Formatting ───────────────────────────────────────────────────


class TestFormatPayload:
    def test_slack_format(self):
        from brynq.integration_hub import _format_payload
        result = _format_payload("slack", "task.done", {"text": "hello"})
        assert "text" in result
        assert "task.done" in result["text"]

    def test_discord_format(self):
        from brynq.integration_hub import _format_payload
        result = _format_payload("discord", "task.done", {"message": "hi"})
        assert "content" in result
        assert "task.done" in result["content"]

    def test_github_format(self):
        from brynq.integration_hub import _format_payload
        result = _format_payload("github", "deploy", {"ref": "main"})
        assert result["event_type"] == "deploy"
        assert result["client_payload"]["ref"] == "main"

    def test_jira_format(self):
        from brynq.integration_hub import _format_payload
        result = _format_payload("jira", "task.created", {"title": "Bug fix"})
        assert result["fields"]["summary"] == "Bug fix"

    def test_linear_format(self):
        from brynq.integration_hub import _format_payload
        result = _format_payload("linear", "issue", {"title": "New feature"})
        assert result["title"] == "New feature"

    def test_notion_format(self):
        from brynq.integration_hub import _format_payload
        result = _format_payload("notion", "page", {"title": "Notes"})
        assert "properties" in result

    def test_custom_webhook_passthrough(self):
        from brynq.integration_hub import _format_payload
        result = _format_payload("custom_webhook", "ping", {"data": 42})
        assert result["event_type"] == "ping"
        assert result["payload"]["data"] == 42


# ── Receive Events (inbound) ─────────────────────────────────────────────


class TestIncomingEvents:
    def test_register_and_process(self):
        from brynq.integration_hub import (
            register_integration, register_incoming_handler, process_incoming,
        )
        register_integration("in1", "github", {})

        received = []
        register_incoming_handler("in1", "issue.created", lambda et, p: received.append((et, p)))

        result = process_incoming("in1", "issue.created", {"title": "Bug"})
        assert result["handler_count"] == 1
        assert result["successes"] == 1
        assert len(received) == 1
        assert received[0] == ("issue.created", {"title": "Bug"})

    def test_process_no_handlers(self):
        from brynq.integration_hub import register_integration, process_incoming
        register_integration("in2", "slack", {})
        result = process_incoming("in2", "unknown.event", {})
        assert result["handler_count"] == 0
        assert result["successes"] == 0

    def test_process_handler_error(self):
        from brynq.integration_hub import (
            register_integration, register_incoming_handler, process_incoming,
        )
        register_integration("in3", "slack", {})

        def bad_handler(et, p):
            raise ValueError("boom")

        register_incoming_handler("in3", "msg", bad_handler)
        result = process_incoming("in3", "msg", {})
        assert result["handler_count"] == 1
        assert result["successes"] == 0
        assert len(result["errors"]) == 1
        assert "boom" in result["errors"][0]

    def test_process_nonexistent_integration(self):
        from brynq.integration_hub import process_incoming
        result = process_incoming("nope", "msg", {})
        assert "error" in result

    def test_process_paused_integration(self):
        from brynq.integration_hub import (
            register_integration, disable_integration, process_incoming,
        )
        register_integration("paused1", "slack", {})
        disable_integration("paused1")
        result = process_incoming("paused1", "msg", {})
        assert "error" in result

    def test_multiple_handlers(self):
        from brynq.integration_hub import (
            register_integration, register_incoming_handler, process_incoming,
        )
        register_integration("multi", "github", {})

        calls = []
        register_incoming_handler("multi", "push", lambda et, p: calls.append("a"))
        register_incoming_handler("multi", "push", lambda et, p: calls.append("b"))

        result = process_incoming("multi", "push", {})
        assert result["handler_count"] == 2
        assert result["successes"] == 2
        assert calls == ["a", "b"]

    def test_handler_returns_id(self):
        from brynq.integration_hub import register_integration, register_incoming_handler
        register_integration("hid", "slack", {})
        hid = register_incoming_handler("hid", "msg", lambda et, p: None)
        assert isinstance(hid, str)
        assert hid.startswith("handler-")

    def test_incoming_updates_stats(self):
        from brynq.integration_hub import (
            register_integration, register_incoming_handler,
            process_incoming, get_integration,
        )
        register_integration("stat1", "slack", {})
        register_incoming_handler("stat1", "msg", lambda et, p: None)
        process_incoming("stat1", "msg", {"text": "hi"})
        rec = get_integration("stat1")
        assert rec["stats"]["events_received"] == 1
        assert rec["stats"]["last_event_at"] is not None


# ── Templates ────────────────────────────────────────────────────────────


class TestTemplates:
    def test_github_template(self):
        from brynq.integration_hub import github_template
        t = github_template()
        assert t["service_type"] == "github"
        assert "issue.created" in t["event_mappings"]
        assert "pull_request.merged" in t["event_mappings"]

    def test_slack_template(self):
        from brynq.integration_hub import slack_template
        t = slack_template()
        assert t["service_type"] == "slack"
        assert "message.posted" in t["event_mappings"]
        assert "task.completed" in t["event_mappings"]

    def test_discord_template(self):
        from brynq.integration_hub import discord_template
        t = discord_template()
        assert t["service_type"] == "discord"
        assert "message.posted" in t["event_mappings"]

    def test_get_template_known(self):
        from brynq.integration_hub import get_template
        assert get_template("github") is not None
        assert get_template("slack") is not None
        assert get_template("discord") is not None

    def test_get_template_unknown(self):
        from brynq.integration_hub import get_template
        assert get_template("jira") is None
        assert get_template("foobar") is None


# ── Stats ────────────────────────────────────────────────────────────────


class TestStats:
    def test_stats_not_found(self):
        from brynq.integration_hub import get_integration_stats
        result = get_integration_stats("nope")
        assert "error" in result

    def test_stats_basic(self):
        from brynq.integration_hub import register_integration, get_integration_stats
        register_integration("st1", "slack", {})
        stats = get_integration_stats("st1")
        assert stats["name"] == "st1"
        assert stats["events_sent"] == 0
        assert stats["events_received"] == 0
        assert stats["errors"] == 0
        assert stats["uptime_seconds"] >= 0

    def test_stats_uptime_paused_is_zero(self):
        from brynq.integration_hub import register_integration, disable_integration, get_integration_stats
        register_integration("st2", "slack", {})
        disable_integration("st2")
        stats = get_integration_stats("st2")
        assert stats["uptime_seconds"] == 0

    def test_all_stats(self):
        from brynq.integration_hub import register_integration, disable_integration, get_all_stats
        register_integration("x1", "slack", {})
        register_integration("x2", "github", {})
        disable_integration("x2")
        summary = get_all_stats()
        assert summary["total_integrations"] == 2
        assert summary["active"] == 1
        assert summary["paused"] == 1
        assert len(summary["integrations"]) == 2


# ── Connection Test ──────────────────────────────────────────────────────


class TestConnectionTest:
    def test_connection_test_not_found(self):
        from brynq.integration_hub import test_integration
        result = test_integration("nope")
        assert "error" in result

    def test_connection_test_no_url(self):
        from brynq.integration_hub import register_integration, test_integration
        register_integration("no-url", "slack", {})
        result = test_integration("no-url")
        assert result["success"] is False
        assert "No URL" in result["detail"]

    def test_connection_test_success(self, monkeypatch):
        import brynq.integration_hub as ih
        from brynq.integration_hub import register_integration, test_integration

        register_integration("test-ok", "custom_webhook", {"webhook_url": "https://example.com"})
        monkeypatch.setattr(ih, "_do_http_post", lambda url, payload, headers=None, timeout=10: (True, "pong"))

        result = test_integration("test-ok")
        assert result["success"] is True
        assert result["latency_ms"] >= 0
        assert "pong" in result["detail"]

    def test_connection_test_failure(self, monkeypatch):
        import brynq.integration_hub as ih
        from brynq.integration_hub import register_integration, test_integration

        register_integration("test-fail", "custom_webhook", {"webhook_url": "https://example.com"})
        monkeypatch.setattr(ih, "_do_http_post", lambda url, payload, headers=None, timeout=10: (False, "connection refused"))

        result = test_integration("test-fail")
        assert result["success"] is False


# ── Event Log ────────────────────────────────────────────────────────────


class TestEventLog:
    def test_outbound_event_logged(self, monkeypatch):
        import brynq.integration_hub as ih
        from brynq.integration_hub import register_integration, send_to_integration, EVENTS_FILE

        register_integration("log1", "custom_webhook", {"webhook_url": "https://example.com"})
        monkeypatch.setattr(ih, "_do_http_post", lambda url, payload, headers=None, timeout=10: (True, "ok"))

        send_to_integration("log1", "test.event", {"key": "val"})

        from brynq.server import _read_jsonl_sync
        events = _read_jsonl_sync(EVENTS_FILE)
        assert len(events) >= 1
        assert events[-1]["integration"] == "log1"
        assert events[-1]["direction"] == "outbound"

    def test_inbound_event_logged(self):
        from brynq.integration_hub import (
            register_integration, register_incoming_handler,
            process_incoming, EVENTS_FILE,
        )
        register_integration("log2", "github", {})
        register_incoming_handler("log2", "push", lambda et, p: None)
        process_incoming("log2", "push", {"ref": "main"})

        from brynq.server import _read_jsonl_sync
        events = _read_jsonl_sync(EVENTS_FILE)
        assert len(events) >= 1
        assert events[-1]["direction"] == "inbound"
