"""Tests for data source connectors."""

from __future__ import annotations

from pathlib import Path
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import httpx
import pytest

from redhat_oracle.core.config import OracleConfig
from redhat_oracle.core.sources.github import (
    _list_org_repos,
    sync_github,
)
from redhat_oracle.core.sources.github import (
    _should_include as gh_should_include,
)
from redhat_oracle.core.sources.gitlab import (
    _list_group_projects,
    sync_gitlab,
)
from redhat_oracle.core.sources.gitlab import (
    _should_include as gl_should_include,
)
from redhat_oracle.core.sources.slack import (
    _slack_api,
    sync_slack,
)

_DUMMY_REQUEST = httpx.Request("GET", "https://example.com")


def _response(status_code: int, **kwargs: Any) -> httpx.Response:
    """Create an httpx.Response with a dummy request attached."""
    return httpx.Response(status_code, request=_DUMMY_REQUEST, **kwargs)


# --- GitHub tests ---


class TestGitHubShouldInclude:
    def test_include_all_when_no_patterns(self) -> None:
        from redhat_oracle.core.config import GitHubOrgConfig

        org = GitHubOrgConfig(name="test")
        assert gh_should_include("any-repo", org) is True

    def test_exclude_matches(self) -> None:
        from redhat_oracle.core.config import GitHubOrgConfig

        org = GitHubOrgConfig(name="test", exclude=["*.old"])
        assert gh_should_include("my-repo.old", org) is False
        assert gh_should_include("my-repo", org) is True

    def test_include_only_matches(self) -> None:
        from redhat_oracle.core.config import GitHubOrgConfig

        org = GitHubOrgConfig(name="test", include=["insights-*"])
        assert gh_should_include("insights-core", org) is True
        assert gh_should_include("other-repo", org) is False

    def test_exclude_takes_precedence(self) -> None:
        from redhat_oracle.core.config import GitHubOrgConfig

        org = GitHubOrgConfig(name="test", include=["*"], exclude=["*.bak"])
        assert gh_should_include("repo.bak", org) is False


class TestGitHubListRepos:
    @pytest.mark.asyncio
    async def test_list_org_repos_paginates(self) -> None:
        page1 = [{"name": "repo1"}, {"name": "repo2"}]
        page2 = [{"name": "repo3"}]
        page3: list[dict[str, str]] = []

        responses = [
            _response(200, json=page1),
            _response(200, json=page2),
            _response(200, json=page3),
        ]

        mock_client = AsyncMock(spec=httpx.AsyncClient)
        mock_client.get = AsyncMock(side_effect=responses)

        repos = await _list_org_repos(mock_client, "test-org", "ghp_token")
        assert len(repos) == 3
        assert repos[0]["name"] == "repo1"
        assert repos[2]["name"] == "repo3"


class TestGitHubSync:
    @pytest.mark.asyncio
    async def test_sync_clones_repos(self, tmp_path: Path) -> None:
        config = OracleConfig(
            sources={  # type: ignore[arg-type]
                "github": {
                    "token": "ghp_test",
                    "use_ssh": False,
                    "orgs": [{"name": "test-org"}],
                },
                "max_concurrent_clones": 2,
            },
            data_dir=str(tmp_path),
        )

        mock_repos = [
            {"name": "repo1", "clone_url": "https://github.com/test-org/repo1.git",
             "ssh_url": "git@github.com:test-org/repo1.git"},
        ]

        with (
            patch(
                "redhat_oracle.core.sources.github._list_org_repos",
                new_callable=AsyncMock,
                return_value=mock_repos,
            ),
            patch(
                "redhat_oracle.core.sources.github._git_clone",
            ) as mock_clone,
        ):
            stats = await sync_github(config)

        assert stats["synced"] == 1
        assert stats["failed"] == 0
        mock_clone.assert_called_once()

    @pytest.mark.asyncio
    async def test_sync_respects_semaphore(self, tmp_path: Path) -> None:
        config = OracleConfig(
            sources={  # type: ignore[arg-type]
                "github": {
                    "token": "ghp_test",
                    "use_ssh": False,
                    "orgs": [{"name": "test-org"}],
                },
                "max_concurrent_clones": 2,
            },
            data_dir=str(tmp_path),
        )

        mock_repos = [
            {"name": f"repo{i}", "clone_url": f"https://github.com/test-org/repo{i}.git",
             "ssh_url": f"git@github.com:test-org/repo{i}.git"}
            for i in range(5)
        ]

        concurrent_count = 0
        max_concurrent = 0

        original_clone = MagicMock()

        def tracking_clone(url: str, dest: Path, kwargs: dict[str, Any]) -> None:
            nonlocal concurrent_count, max_concurrent
            concurrent_count += 1
            max_concurrent = max(max_concurrent, concurrent_count)
            concurrent_count -= 1
            original_clone(url, dest, kwargs)

        with (
            patch(
                "redhat_oracle.core.sources.github._list_org_repos",
                new_callable=AsyncMock,
                return_value=mock_repos,
            ),
            patch(
                "redhat_oracle.core.sources.github._git_clone",
                side_effect=tracking_clone,
            ),
        ):
            stats = await sync_github(config)

        assert stats["synced"] == 5
        assert stats["failed"] == 0


# --- GitLab tests ---


class TestGitLabShouldInclude:
    def test_include_all_when_no_patterns(self) -> None:
        from redhat_oracle.core.config import GitLabGroupConfig

        group = GitLabGroupConfig(name="test")
        assert gl_should_include("any-project", group) is True


class TestGitLabListProjects:
    @pytest.mark.asyncio
    async def test_list_group_projects_paginates(self) -> None:
        page1 = [{"name": "proj1"}]
        page2: list[dict[str, str]] = []

        responses = [
            _response(200, json=page1),
            _response(200, json=page2),
        ]

        mock_client = AsyncMock(spec=httpx.AsyncClient)
        mock_client.get = AsyncMock(side_effect=responses)

        projects = await _list_group_projects(
            mock_client, "https://gitlab.example.com", "mygroup", "glpat-test"
        )
        assert len(projects) == 1
        assert projects[0]["name"] == "proj1"


class TestGitLabSync:
    @pytest.mark.asyncio
    async def test_sync_clones_projects(self, tmp_path: Path) -> None:
        config = OracleConfig(
            sources={  # type: ignore[arg-type]
                "gitlab": {
                    "token": "glpat-test",
                    "use_ssh": False,
                    "instances": [
                        {
                            "url": "https://gitlab.example.com",
                            "groups": [{"name": "my-group"}],
                        }
                    ],
                },
                "max_concurrent_clones": 2,
            },
            data_dir=str(tmp_path),
        )

        mock_projects = [
            {
                "name": "proj1",
                "http_url_to_repo": "https://gitlab.example.com/my-group/proj1.git",
                "ssh_url_to_repo": "git@gitlab.example.com:my-group/proj1.git",
                "path_with_namespace": "my-group/proj1",
            },
        ]

        with (
            patch(
                "redhat_oracle.core.sources.gitlab._list_group_projects",
                new_callable=AsyncMock,
                return_value=mock_projects,
            ),
            patch(
                "redhat_oracle.core.sources.gitlab._git_clone",
            ) as mock_clone,
        ):
            stats = await sync_gitlab(config)

        assert stats["synced"] == 1
        assert stats["failed"] == 0
        mock_clone.assert_called_once()


# --- Slack tests ---


class TestSlackApi:
    @pytest.mark.asyncio
    async def test_rate_limit_retry(self) -> None:
        rate_limited = _response(429, headers={"Retry-After": "0"})
        success = _response(200, json={"ok": True, "result": "data"})

        mock_client = AsyncMock(spec=httpx.AsyncClient)
        mock_client.get = AsyncMock(side_effect=[rate_limited, success])

        with patch("redhat_oracle.core.sources.slack.asyncio.sleep", new_callable=AsyncMock):
            result = await _slack_api(mock_client, "test.method", "xoxb-test")
        assert result["ok"] is True
        assert mock_client.get.call_count == 2

    @pytest.mark.asyncio
    async def test_rate_limit_exhaustion(self) -> None:
        rate_limited = _response(429, headers={"Retry-After": "0"})

        mock_client = AsyncMock(spec=httpx.AsyncClient)
        mock_client.get = AsyncMock(return_value=rate_limited)

        with (
            patch("redhat_oracle.core.sources.slack.asyncio.sleep", new_callable=AsyncMock),
            pytest.raises(RuntimeError, match="rate limit exceeded"),
        ):
            await _slack_api(mock_client, "test.method", "xoxb-test", max_retries=2)


class TestSlackSync:
    @pytest.mark.asyncio
    async def test_sync_returns_empty_without_token(self) -> None:
        config = OracleConfig(sources={"slack": {"token": None}})  # type: ignore[arg-type]
        result = await sync_slack(config)
        assert result == {"messages": 0, "chunks": 0, "days_processed": 0, "days_skipped": 0}

    @pytest.mark.asyncio
    async def test_sync_fetches_messages(self, tmp_path: Path) -> None:
        import time

        # Use a timestamp within the last day so the day-by-day fetch finds it
        recent_ts = str(time.time() - 3600)  # 1 hour ago

        config = OracleConfig(
            sources={  # type: ignore[arg-type]
                "slack": {"token": "xoxb-test", "channels": ["general"], "history_days": 2}
            },
            data_dir=str(tmp_path),
        )

        channels_resp = {"ok": True, "channels": [{"id": "C123", "name": "general"}]}
        channel_info_resp = {"ok": True, "channel": {"id": "C123", "name": "general"}}
        messages_resp = {
            "ok": True,
            "messages": [
                {"user": "U123", "text": "Hello world", "ts": recent_ts},
            ],
        }
        empty_resp = {"ok": True, "messages": []}
        user_resp = {
            "ok": True,
            "user": {"real_name": "Test User", "profile": {"display_name": "tester"}},
        }

        call_count = {"history": 0}

        async def mock_get(url: str, **kwargs: Any) -> httpx.Response:
            if "users.conversations" in url:
                return _response(200, json=channels_resp)
            elif "conversations.info" in url:
                return _response(200, json=channel_info_resp)
            elif "conversations.history" in url:
                call_count["history"] += 1
                # Only return messages on the first call (today)
                if call_count["history"] == 1:
                    return _response(200, json=messages_resp)
                return _response(200, json=empty_resp)
            elif "users.info" in url:
                return _response(200, json=user_resp)
            return _response(200, json={"ok": True})

        mock_store = MagicMock()
        mock_store.upsert_chunks = MagicMock(return_value=1)

        with (
            patch("httpx.AsyncClient") as mock_client_cls,
            patch("redhat_oracle.core.sources.slack.asyncio.sleep", new_callable=AsyncMock),
        ):
            mock_client = AsyncMock()
            mock_client.get = mock_get
            mock_client.__aenter__ = AsyncMock(return_value=mock_client)
            mock_client.__aexit__ = AsyncMock(return_value=None)
            mock_client_cls.return_value = mock_client

            result = await sync_slack(config, store=mock_store)

        assert result["messages"] == 1
        assert result["chunks"] >= 1
        assert result["days_processed"] >= 1
        mock_store.upsert_chunks.assert_called()
