import uuid
from datetime import datetime
from unittest.mock import Mock

import httpx
import pytest
from sqlalchemy import select
from sqlalchemy.orm import Session

from models.model import DifySetup
from services import telemetry_service
from services.telemetry_service import CommunityTelemetryService


@pytest.fixture
def telemetry_enabled(monkeypatch: pytest.MonkeyPatch):
    monkeypatch.setattr(telemetry_service.dify_config, "EDITION", "SELF_HOSTED")
    monkeypatch.setattr(telemetry_service.dify_config, "ENTERPRISE_ENABLED", False)
    monkeypatch.setattr(telemetry_service.dify_config, "DISABLE_TELEMETRY", False)
    monkeypatch.setattr(telemetry_service.dify_config, "DO_NOT_TRACK", False)
    monkeypatch.setattr(telemetry_service.dify_config, "CI", False)
    monkeypatch.setattr(telemetry_service.dify_config, "TELEMETRY_ENDPOINT", "https://telemetry.example.test/v1/events")
    monkeypatch.setattr(
        telemetry_service.dify_config,
        "TELEMETRY_FALLBACK_ENDPOINT",
        "https://telemetry-cn.example.test/v1/events",
    )
    monkeypatch.setattr(telemetry_service.dify_config, "TELEMETRY_TIMEOUT_SECONDS", 2)


def test_telemetry_is_disabled_for_enterprise(monkeypatch: pytest.MonkeyPatch):
    monkeypatch.setattr(telemetry_service.dify_config, "EDITION", "SELF_HOSTED")
    monkeypatch.setattr(telemetry_service.dify_config, "ENTERPRISE_ENABLED", True)

    assert CommunityTelemetryService._is_enabled() is False


@pytest.mark.parametrize(
    ("setting", "value"),
    [
        ("EDITION", "CLOUD"),
        ("DISABLE_TELEMETRY", True),
        ("DO_NOT_TRACK", True),
        ("CI", True),
        ("TELEMETRY_ENDPOINT", ""),
    ],
)
def test_telemetry_is_disabled_when_a_required_condition_is_not_met(
    telemetry_enabled, monkeypatch: pytest.MonkeyPatch, setting: str, value: str | bool
):
    monkeypatch.setattr(telemetry_service.dify_config, setting, value)

    assert CommunityTelemetryService._is_enabled() is False


@pytest.mark.parametrize("sqlite_session", [(DifySetup,)], indirect=True)
def test_reporting_without_setup_is_skipped(sqlite_session: Session, telemetry_enabled):
    assert CommunityTelemetryService.report_install(session=sqlite_session) is False
    assert CommunityTelemetryService.report_heartbeat(session=sqlite_session) is False


@pytest.mark.parametrize("sqlite_session", [(DifySetup,)], indirect=True)
def test_report_install_marks_reported_at(sqlite_session: Session, telemetry_enabled, monkeypatch: pytest.MonkeyPatch):
    setup = DifySetup(version="installed-version", instance_id="d246c3a1-350b-406c-92c7-6043df680758")
    sqlite_session.add(setup)
    sqlite_session.commit()
    monkeypatch.setattr(telemetry_service.dify_config.project, "version", "running-version")

    sent_payloads: list[dict[str, str | int]] = []

    def fake_post(url: str, json: dict[str, str | int], timeout: int):
        sent_payloads.append(json)
        return httpx.Response(204, request=httpx.Request("POST", url))

    monkeypatch.setattr(telemetry_service.httpx, "post", fake_post)

    assert CommunityTelemetryService.report_install(session=sqlite_session) is True

    saved_setup = sqlite_session.scalar(select(DifySetup))
    assert saved_setup is not None
    assert saved_setup.install_reported_at is not None
    assert sent_payloads[0]["event"] == "install"
    assert sent_payloads[0]["instance_id"] == setup.instance_id
    assert sent_payloads[0]["version"] == "installed-version"
    assert "installed_at" in sent_payloads[0]


@pytest.mark.parametrize("sqlite_session", [(DifySetup,)], indirect=True)
def test_report_install_generates_missing_instance_id(
    sqlite_session: Session, telemetry_enabled, monkeypatch: pytest.MonkeyPatch
):
    setup = DifySetup(version="installed-version")
    sqlite_session.add(setup)
    sqlite_session.commit()
    monkeypatch.setattr(
        telemetry_service.httpx,
        "post",
        lambda url, json, timeout: httpx.Response(204, request=httpx.Request("POST", url)),
    )

    assert CommunityTelemetryService.report_install(session=sqlite_session) is True

    assert setup.instance_id is not None
    assert str(uuid.UUID(setup.instance_id)) == setup.instance_id


@pytest.mark.parametrize("sqlite_session", [(DifySetup,)], indirect=True)
def test_report_heartbeat_generates_missing_instance_id(
    sqlite_session: Session, telemetry_enabled, monkeypatch: pytest.MonkeyPatch
):
    setup = DifySetup(version="1.0.0", install_reported_at=datetime(2026, 7, 12, 8, 0, 0))
    sqlite_session.add(setup)
    sqlite_session.commit()
    monkeypatch.setattr(
        telemetry_service.httpx,
        "post",
        lambda url, json, timeout: httpx.Response(204, request=httpx.Request("POST", url)),
    )

    assert (
        CommunityTelemetryService.report_heartbeat(session=sqlite_session, now=datetime(2026, 7, 13, 12, 0, 0)) is True
    )

    assert setup.instance_id is not None
    assert str(uuid.UUID(setup.instance_id)) == setup.instance_id


@pytest.mark.parametrize("sqlite_session", [(DifySetup,)], indirect=True)
def test_report_install_failure_keeps_install_pending(
    sqlite_session: Session, telemetry_enabled, monkeypatch: pytest.MonkeyPatch
):
    setup = DifySetup(version="1.0.0", instance_id="d246c3a1-350b-406c-92c7-6043df680758")
    sqlite_session.add(setup)
    sqlite_session.commit()

    def fake_post(url: str, json: dict[str, str | int], timeout: int):
        raise httpx.ConnectError("offline", request=httpx.Request("POST", url))

    monkeypatch.setattr(telemetry_service.httpx, "post", fake_post)

    assert CommunityTelemetryService.report_install(session=sqlite_session) is False

    saved_setup = sqlite_session.scalar(select(DifySetup))
    assert saved_setup is not None
    assert saved_setup.install_reported_at is None


@pytest.mark.parametrize("sqlite_session", [(DifySetup,)], indirect=True)
def test_report_install_uses_fallback_endpoint_after_network_failure(
    sqlite_session: Session, telemetry_enabled, monkeypatch: pytest.MonkeyPatch
):
    setup = DifySetup(version="1.0.0", instance_id="d246c3a1-350b-406c-92c7-6043df680758")
    sqlite_session.add(setup)
    sqlite_session.commit()

    urls: list[str] = []

    def fake_post(url: str, json: dict[str, str | int], timeout: int):
        urls.append(url)
        if url == telemetry_service.dify_config.TELEMETRY_ENDPOINT:
            raise httpx.ConnectError("offline", request=httpx.Request("POST", url))
        return httpx.Response(204, request=httpx.Request("POST", url))

    monkeypatch.setattr(telemetry_service.httpx, "post", fake_post)

    assert CommunityTelemetryService.report_install(session=sqlite_session) is True
    assert urls == [
        telemetry_service.dify_config.TELEMETRY_ENDPOINT,
        telemetry_service.dify_config.TELEMETRY_FALLBACK_ENDPOINT,
    ]


@pytest.mark.parametrize("sqlite_session", [(DifySetup,)], indirect=True)
def test_report_install_does_not_use_fallback_endpoint_after_http_error(
    sqlite_session: Session, telemetry_enabled, monkeypatch: pytest.MonkeyPatch
):
    setup = DifySetup(version="1.0.0", instance_id="d246c3a1-350b-406c-92c7-6043df680758")
    sqlite_session.add(setup)
    sqlite_session.commit()

    post_mock = Mock(
        return_value=httpx.Response(
            500,
            request=httpx.Request("POST", telemetry_service.dify_config.TELEMETRY_ENDPOINT),
        )
    )
    monkeypatch.setattr(telemetry_service.httpx, "post", post_mock)

    assert CommunityTelemetryService.report_install(session=sqlite_session) is False
    post_mock.assert_called_once()


@pytest.mark.parametrize("sqlite_session", [(DifySetup,)], indirect=True)
def test_report_heartbeat_retries_pending_install_before_heartbeat(
    sqlite_session: Session, telemetry_enabled, monkeypatch: pytest.MonkeyPatch
):
    setup = DifySetup(version="installed-version", instance_id="d246c3a1-350b-406c-92c7-6043df680758")
    sqlite_session.add(setup)
    sqlite_session.commit()
    monkeypatch.setattr(telemetry_service.dify_config.project, "version", "running-version")

    sent_payloads: list[dict[str, str | int]] = []

    def fake_post(url: str, json: dict[str, str | int], timeout: int):
        sent_payloads.append(json)
        return httpx.Response(204, request=httpx.Request("POST", url))

    monkeypatch.setattr(telemetry_service.httpx, "post", fake_post)
    now = datetime(2026, 7, 13, 0, 0, 0)
    assert CommunityTelemetryService.report_heartbeat(session=sqlite_session, now=now) is True

    saved_setup = sqlite_session.scalar(select(DifySetup))
    assert saved_setup is not None
    assert saved_setup.install_reported_at is not None
    assert saved_setup.last_heartbeat_at == now
    assert [(payload["event"], payload["version"]) for payload in sent_payloads] == [
        ("install", "installed-version"),
        ("heartbeat", "running-version"),
    ]


@pytest.mark.parametrize("sqlite_session", [(DifySetup,)], indirect=True)
def test_report_heartbeat_skips_when_already_sent_today(
    sqlite_session: Session, telemetry_enabled, monkeypatch: pytest.MonkeyPatch
):
    setup = DifySetup(
        version="1.0.0",
        instance_id="d246c3a1-350b-406c-92c7-6043df680758",
        install_reported_at=datetime(2026, 7, 13, 8, 0, 0),
        last_heartbeat_at=datetime(2026, 7, 13, 9, 0, 0),
    )
    sqlite_session.add(setup)
    sqlite_session.commit()

    post_mock = Mock()
    monkeypatch.setattr(telemetry_service.httpx, "post", post_mock)

    assert (
        CommunityTelemetryService.report_heartbeat(session=sqlite_session, now=datetime(2026, 7, 13, 12, 0, 0)) is False
    )
    post_mock.assert_not_called()


@pytest.mark.parametrize("sqlite_session", [(DifySetup,)], indirect=True)
def test_report_heartbeat_failure_does_not_mark_the_day_reported(
    sqlite_session: Session, telemetry_enabled, monkeypatch: pytest.MonkeyPatch
):
    setup = DifySetup(
        version="1.0.0",
        instance_id="d246c3a1-350b-406c-92c7-6043df680758",
        install_reported_at=datetime(2026, 7, 13, 8, 0, 0),
    )
    sqlite_session.add(setup)
    sqlite_session.commit()

    def fake_post(url: str, json: dict[str, str | int], timeout: int):
        raise httpx.ConnectError("offline", request=httpx.Request("POST", url))

    monkeypatch.setattr(telemetry_service.httpx, "post", fake_post)

    assert (
        CommunityTelemetryService.report_heartbeat(session=sqlite_session, now=datetime(2026, 7, 13, 12, 0, 0)) is False
    )
    assert setup.last_heartbeat_at is None


def test_send_event_skips_when_telemetry_is_disabled(monkeypatch: pytest.MonkeyPatch):
    monkeypatch.setattr(telemetry_service.dify_config, "DISABLE_TELEMETRY", True)
    post_mock = Mock()
    monkeypatch.setattr(telemetry_service.httpx, "post", post_mock)

    assert CommunityTelemetryService._send_event({"event": "heartbeat"}) is False
    post_mock.assert_not_called()


def test_send_event_skips_an_empty_fallback_endpoint(telemetry_enabled, monkeypatch: pytest.MonkeyPatch):
    monkeypatch.setattr(telemetry_service.dify_config, "TELEMETRY_FALLBACK_ENDPOINT", "")

    def fake_post(url: str, json: dict[str, str], timeout: int):
        raise httpx.ConnectError("offline", request=httpx.Request("POST", url))

    monkeypatch.setattr(telemetry_service.httpx, "post", fake_post)

    assert CommunityTelemetryService._send_event({"event": "heartbeat"}) is False


def test_send_event_does_not_retry_the_same_endpoint(telemetry_enabled, monkeypatch: pytest.MonkeyPatch):
    monkeypatch.setattr(
        telemetry_service.dify_config,
        "TELEMETRY_FALLBACK_ENDPOINT",
        telemetry_service.dify_config.TELEMETRY_ENDPOINT,
    )
    post_mock = Mock(
        return_value=httpx.Response(
            204,
            request=httpx.Request("POST", telemetry_service.dify_config.TELEMETRY_ENDPOINT),
        )
    )
    monkeypatch.setattr(telemetry_service.httpx, "post", post_mock)

    assert CommunityTelemetryService._send_event({"event": "heartbeat"}) is True
    post_mock.assert_called_once()


def test_heartbeat_is_not_due_without_instance_id():
    setup = DifySetup(version="1.0.0")

    assert CommunityTelemetryService._is_heartbeat_due(setup, datetime(2026, 7, 13, 12, 0, 0)) is False


@pytest.mark.parametrize(
    ("value", "expected"),
    [
        ("Linux", "linux"),
        ("Plan9", "unknown"),
    ],
)
def test_normalize_os(value: str, expected: str):
    assert CommunityTelemetryService._normalize_os(value) == expected


@pytest.mark.parametrize(
    ("value", "expected"),
    [
        ("x86_64", "amd64"),
        ("aarch64", "arm64"),
        ("armv7l", "arm"),
        ("i686", "386"),
        ("riscv64", "unknown"),
    ],
)
def test_normalize_arch(value: str, expected: str):
    assert CommunityTelemetryService._normalize_arch(value) == expected
