from unittest.mock import MagicMock, patch

import pytest
import responses
from ghapi.all import GhApi
from model_bakery import baker

from libraries.github import (
    GithubAPIClient,
    GithubDataParser,
    LibraryUpdater,
)
from libraries.models import Category, Issue, Library, PullRequest

"""GithubAPIClient Tests"""


@pytest.fixture
def github_api_client():
    return GithubAPIClient()


@pytest.fixture(scope="function")
def mock_api() -> GhApi:
    """Fixture that mocks the GitHub API."""
    with patch("libraries.github_new.GhApi") as mock_api_class:
        yield mock_api_class.return_value


@pytest.fixture
def github_api_client_mock():
    """ """
    mock = MagicMock()
    return mock


def test_initialize_api():
    """Test the initialize_api method of GitHubAPIClient."""
    api = GithubAPIClient().initialize_api()
    assert isinstance(api, GhApi)


def test_get_blob(github_api_client):
    """Test the get_blob method of GitHubAPIClient."""
    github_api_client.api.git.get_blob = MagicMock(
        return_value={"sha": "12345", "content": "example content", "encoding": "utf-8"}
    )
    result = github_api_client.get_blob(repo_slug="sample_repo", file_sha="12345")
    assert result == {"sha": "12345", "content": "example content", "encoding": "utf-8"}
    github_api_client.api.git.get_blob.assert_called_with(
        owner=github_api_client.owner, repo="sample_repo", file_sha="12345"
    )


###########################################################################
# Something is up with this test, it causes Pytest to fail spectacularly
# using Python 3.11.  Commenting it out for now.  - Frank
###########################################################################
# @pytest.mark.xfail(reason="Something up with bytes")
# @responses.activate
# def test_get_gitmodules(github_api_client):
#     """Test the get_gitmodules method of GitHubAPIClient."""
#     sample_ref_response = {
#         "object": {
#             "sha": "12345",
#         }
#     }
#     sample_tree_response = {
#         "tree": [
#             {
#                 "path": ".gitmodules",
#                 "sha": "67890",
#             }
#         ]
#     }

#     sample_content = "sample content"
#     sample_blob_response = {
#         "content": base64.b64encode(sample_content.encode("utf-8")).decode("utf-8")
#     }

#     # Set up the mocked API responses
#     ref_url = f"https://api.github.com/repos/{github_api_client.owner}/{github_api_client.repo_slug}/git/ref/{github_api_client.ref}"
#     tree_url = f"https://api.github.com/repos/{github_api_client.owner}/{github_api_client.repo_slug}/git/trees/12345"

#     responses.add(responses.GET, ref_url, json=sample_ref_response, status=200)
#     responses.add(responses.GET, tree_url, json=sample_tree_response, status=200)

#     # Mock the get_blob method
#     github_api_client.get_blob = MagicMock(return_value=sample_blob_response)

#     # Call the get_gitmodules method
#     result = github_api_client.get_gitmodules(repo_slug="sample_repo")

#     # Assert the expected result
#     assert result == sample_content

#     # Check if the API calls were made with the correct arguments
#     assert len(responses.calls) == 2
#     assert responses.calls[0].request.url == ref_url
#     assert responses.calls[1].request.url == tree_url
#     github_api_client.get_blob.assert_called_with(
#         repo_slug="sample_repo", file_sha="67890"
#     )


@responses.activate
def test_get_libraries_json(github_api_client):
    """Test the get_libraries_json method of GitHubAPIClient."""
    repo_slug = "sample_repo"
    url = f"https://raw.githubusercontent.com/{github_api_client.owner}/{repo_slug}/develop/meta/libraries.json"
    sample_json = {"key": "math", "name": "Math"}
    responses.add(
        responses.GET,
        url,
        json=sample_json,
        status=200,
        content_type="application/json",
    )
    result = github_api_client.get_libraries_json(repo_slug=repo_slug)
    assert result == {"key": "math", "name": "Math"}
    assert len(responses.calls) == 1
    assert responses.calls[0].request.url == url


def test_get_ref(github_api_client):
    """Test the get_ref method of GitHubAPIClient."""
    github_api_client.api.git.get_ref = MagicMock(
        return_value={"content": "example content"}
    )
    result = github_api_client.get_ref(repo_slug="sample_repo", ref="head/main")
    assert result == {"content": "example content"}


def test_get_repo(github_api_client):
    """Test the get_repo method of GitHubAPIClient."""
    github_api_client.api.repos.get = MagicMock(
        return_value={"content": "example content"}
    )
    result = github_api_client.get_repo(repo_slug="sample_repo")
    assert result == {"content": "example content"}


"""Parser Tests"""


def test_parse_gitmodules():
    sample_gitmodules = """
[submodule "system"]
    path = libs/system
    url = ../system.git
    fetchRecurseSubmodules = on-demand
    branch = .
[submodule "multi_array"]
    path = libs/multi_array
    url = ../multi_array.git
    fetchRecurseSubmodules = on-demand
    branch = .
"""

    parser = GithubDataParser()
    parsed_data = parser.parse_gitmodules(sample_gitmodules)

    expected_output = [
        {
            "module": "system",
            "url": "system",
        },
        {
            "module": "multi_array",
            "url": "multi_array",
        },
    ]

    assert parsed_data == expected_output


def test_parse_libraries_json():
    sample_libraries_json = {
        "key": "math",
        "name": "Math",
        "authors": [],
        "description": "Sample Description",
        "category": ["Math"],
        "maintainers": [],
        "cxxstd": "14",
    }

    parser = GithubDataParser()
    parser.parse_libraries_json(sample_libraries_json)


def test_extract_names():
    sample = "Tester Testerson <tester -at- gmail.com>"
    expected = ["Tester", "Testerson"]
    result = GithubDataParser().extract_names(sample)
    assert expected == result

    sample = "Tester Testerson"
    expected = ["Tester", "Testerson"]
    result = GithubDataParser().extract_names(sample)
    assert expected == result

    sample = "Tester de Testerson <tester -at- gmail.com>"
    expected = ["Tester de", "Testerson"]
    result = GithubDataParser().extract_names(sample)
    assert expected == result

    sample = "Tester de Testerson"
    expected = ["Tester de", "Testerson"]
    result = GithubDataParser().extract_names(sample)
    assert expected == result

    sample = "Various"
    expected = ["Various", ""]
    result = GithubDataParser().extract_names(sample)
    assert expected == result


def test_extract_email():
    expected = "t_testerson@example.com"
    result = GithubDataParser().extract_email(
        "Tester Testerston <t_testerson -at- example.com>"
    )
    assert expected == result

    expected = "t.t.testerson@example.com"
    result = GithubDataParser().extract_email(
        "Tester Testerston <t.t.testerson -at- example.com>"
    )
    assert expected == result

    expected = "t.t.testerson@example.sample.com"
    result = GithubDataParser().extract_email(
        "Tester Testerston <t.t.testerson -at- example.sample.com> "
    )
    assert expected == result

    expected = None
    result = GithubDataParser().extract_email("Tester Testeron")
    assert expected == result

    expected = "t_tester@example.com"
    result = GithubDataParser().extract_email(
        "Tester Testerston <t -underscore- tester -at- example -dot- com> "
    )
    assert expected == result

    expected = "tester@example.com"
    result = GithubDataParser().extract_email(
        "Tester Testerston <tester - at - example.com> "
    )
    assert expected == result


def test_extract_contributor_data():
    sample = "Tester Testerson <tester -at- gmail.com>"
    expected = {
        "valid_email": True,
        "email": "tester@gmail.com",
        "first_name": "Tester",
        "last_name": "Testerson",
    }
    result = GithubDataParser().extract_contributor_data(sample)
    assert expected == result

    sample = "Tester Testerson"
    expected = {
        "valid_email": False,
        "first_name": "Tester",
        "last_name": "Testerson",
    }
    result = GithubDataParser().extract_contributor_data(sample)
    assert expected["valid_email"] is False
    assert expected["first_name"] == result["first_name"]
    assert expected["last_name"] == result["last_name"]
    assert "email" in result


"""LibraryUpdater Tests"""


@pytest.fixture
def mock_gh_api_client():
    client = GithubAPIClient()
    client.get_libraries_json = MagicMock(return_value=None)
    client.get_repo = MagicMock(return_value=None)
    client.get_gitmodules = MagicMock(return_value=b"sample content")
    return client


@pytest.fixture
def library_updater(mock_gh_api_client):
    return LibraryUpdater()


def test_get_library_list(library_updater):
    """Test the get_library_list method of LibraryUpdater."""
    gitmodules = [{"module": "test"}]
    library_updater.client.get_libraries_json = MagicMock(
        return_value=[
            {
                "key": "test",
                "name": "Test Library",
                "description": "Test description",
                "cxxstd": "11",
                "category": ["Test"],
                "authors": ["John Doe"],
                "maintainers": ["Jane Doe"],
            }
        ]
    )
    library_updater.client.get_repo = MagicMock(
        return_value={"html_url": "example.com"}
    )
    expected = [
        {
            "key": "test",
            "name": "Test Library",
            "github_url": "example.com",
            "description": "Test description",
            "cxxstd": "11",
            "last_github_update": None,
            "category": ["Test"],
            "authors": ["John Doe"],
            "maintainers": ["Jane Doe"],
        }
    ]
    result = library_updater.get_library_list(gitmodules=gitmodules)
    assert result == expected


def test_get_library_list_skip(library_updater):
    """Test that the get_library_list method of LibraryUpdater skips the right modules"""
    gitmodules = [{"module": "litre"}]
    result = library_updater.get_library_list(gitmodules=gitmodules)
    assert result == []


def test_update_authors(library_updater, user, library_version):
    library = library_version.library
    assert library.authors.exists() is False
    user.claimed = True
    user.email = "t_testerson@example.com"
    user.save()

    library_updater.update_authors(
        library,
        authors=[
            "Tester Testerston <t_testerson -at- example.com>",
            "Tester2 Testerson2",
        ],
    )
    library.refresh_from_db()
    assert library.authors.exists()
    assert library.authors.filter(email="t_testerson@example.com").exists()
    assert library.authors.filter(email="tester2_testerson2@example.com").exists()


def test_update_maintainers(library_updater, user, library_version):
    assert library_version.maintainers.exists() is False
    user.claimed = True
    user.email = "t_testerson@example.com"
    user.save()

    library_updater.update_maintainers(
        library_version,
        maintainers=[
            "Tester Testerston <t_testerson -at- example.com>",
            "Tester2 Testerson2",
        ],
    )
    library_version.refresh_from_db()
    assert library_version.maintainers.exists()
    assert library_version.maintainers.filter(email="t_testerson@example.com").exists()
    assert library_version.maintainers.filter(
        email="tester2_testerson2@example.com"
    ).exists()


@pytest.mark.skip("Add this test when we have figured out GH API mocking")
def test_update_libraries(library_updater, version):
    """Test the update_libraries method of LibraryUpdater."""
    assert Library.objects.filter(key="test").exists() is False
    library_updater.parser.parse_gitmodules = MagicMock(return_value=[])
    library_updater.get_library_list = MagicMock(
        return_value=[
            {
                "key": "test",
                "name": "Test Library",
                "github_url": "https://github.com/test/test",
                "description": "Test description",
                "cxxstd": "11",
                "last_github_update": None,
                "category": ["Test"],
                "authors": ["John Doe"],
                "maintainers": ["Jane Doe"],
            }
        ]
    )
    library_updater.update_libraries()
    assert Library.objects.filter(key="test").exists()


def test_update_library(library_updater, version):
    """Test the update_library method of LibraryUpdater."""
    assert Library.objects.filter(key="test").exists() is False
    library_data = {
        "key": "test",
        "name": "Test Library",
        "github_url": "https://github.com/test/test",
        "description": "Test description",
        "cxxstd": "11",
        "last_github_update": None,
        "category": ["Test"],
        "authors": ["John Doe"],
        "maintainers": ["Jane Doe"],
    }
    library_updater.update_library(library_data)
    assert Library.objects.filter(key="test").exists()
    library = Library.objects.get(key="test")
    assert library.categories.filter(name="Test").exists()


def test_update_categories(library, library_updater):
    """Test the update_categories method of LibraryUpdater."""
    assert Category.objects.filter(name="Test").exists() is False
    assert library.categories.filter(name="Test").exists() is False
    library_updater.update_categories(library, ["Test"])
    library.refresh_from_db()
    assert Category.objects.filter(name="Test").exists()
    assert library.categories.filter(name="Test").exists()


def test_update_issues_new(
    tp, library, github_api_repo_issues_response, library_updater
):
    """Test the update_issues method of LibraryUpdater with new issues."""
    new_issues_count = len(github_api_repo_issues_response)
    expected_count = Issue.objects.count() + new_issues_count
    library_updater.client.get_repo_issues = MagicMock(
        return_value=github_api_repo_issues_response
    )
    library_updater.update_issues(library)

    ids = [issue.id for issue in github_api_repo_issues_response]
    issues = Issue.objects.filter(library=library, github_id__in=ids)
    assert Issue.objects.filter(library=library, github_id__in=ids).exists()
    assert (
        Issue.objects.filter(library=library, github_id__in=ids).count()
        == expected_count
    )

    # Test the values of a sample Issue
    gh_issue = github_api_repo_issues_response[0]
    issue = issues.get(github_id=gh_issue.id)
    assert issue.title == gh_issue.title
    assert issue.number == gh_issue.number
    if gh_issue.state == "open":
        assert issue.is_open
    else:
        assert not issue.is_open
    assert issue.data == gh_issue


def test_update_issues_existing(
    tp, library, github_api_repo_issues_response, library_updater
):
    """Test the update_issues method of LibraryUpdater with existing issues."""
    existing_issue_data = github_api_repo_issues_response[0]
    old_title = "Old title"
    issue = baker.make(
        Issue, library=library, github_id=existing_issue_data.id, title=old_title
    )

    # Make sure we are expected one fewer new issue, since we created one in advance
    new_issues_count = len(github_api_repo_issues_response)
    expected_count = Issue.objects.count() + new_issues_count - 1

    library_updater.client.get_repo_issues = MagicMock(
        return_value=github_api_repo_issues_response
    )
    library_updater.update_issues(library)

    assert Issue.objects.count() == expected_count
    ids = [issue.id for issue in github_api_repo_issues_response]
    issues = Issue.objects.filter(library=library, github_id__in=ids)
    assert issues.exists()
    assert issues.count() == expected_count

    # Test that the existing issue updated
    issue.refresh_from_db()
    assert issue.title == existing_issue_data.title


def test_update_issues_long_title(
    tp, library, github_api_repo_issues_response, library_updater
):
    """Test the update_issues method of LibraryUpdater handles long title gracefully"""
    new_issues_count = len(github_api_repo_issues_response)
    expected_count = Issue.objects.count() + new_issues_count
    title = "sample" * 100
    assert len(title) > 255
    expected_title = title[:255]
    assert len(expected_title) <= 255

    github_id = github_api_repo_issues_response[0]["id"]
    github_api_repo_issues_response[0]["title"] = "sample" * 100
    library_updater.client.get_repo_issues = MagicMock(
        return_value=github_api_repo_issues_response
    )
    library_updater.update_issues(library)

    assert Issue.objects.count() == expected_count
    assert Issue.objects.filter(library=library, github_id=github_id).exists()
    issue = Issue.objects.get(library=library, github_id=github_id)
    assert issue.title == expected_title


def test_update_prs_new(tp, library, github_api_repo_prs_response, library_updater):
    """Test that LibraryUpdater.update_prs() imports new PRs appropriately"""
    new_prs_count = len(github_api_repo_prs_response)
    expected_count = PullRequest.objects.count() + new_prs_count

    github_api_repo_prs_response[0]["title"] = "sample" * 100
    library_updater.client.get_repo_prs = MagicMock(
        return_value=github_api_repo_prs_response
    )
    library_updater.update_prs(library)

    assert PullRequest.objects.count() == expected_count
    ids = [pr.id for pr in github_api_repo_prs_response]
    pulls = PullRequest.objects.filter(library=library, github_id__in=ids)
    assert pulls.exists()
    assert pulls.count() == expected_count

    # Test the values of a sample PR
    gh_pull = github_api_repo_prs_response[0]
    pr = pulls.get(github_id=gh_pull.id)
    assert pr.title == gh_pull.title[:255]
    assert pr.number == gh_pull.number
    if gh_pull.state == "open":
        assert pr.is_open
    else:
        assert not pr.is_open
    assert pr.data == gh_pull


def test_update_prs_existing(
    tp, library, github_api_repo_prs_response, library_updater
):
    """Test that LibraryUpdater.update_prs() updates existing PRs when appropriate"""
    existing_pr_data = github_api_repo_prs_response[0]
    old_title = "Old title"
    pull = baker.make(
        PullRequest, library=library, github_id=existing_pr_data.id, title=old_title
    )

    # Make sure we are expected one fewer new PRs, since we created one in advance
    new_prs_count = len(github_api_repo_prs_response)
    expected_count = PullRequest.objects.count() + new_prs_count - 1

    library_updater.client.get_repo_prs = MagicMock(
        return_value=github_api_repo_prs_response
    )
    library_updater.update_prs(library)

    assert PullRequest.objects.count() == expected_count
    ids = [pr.id for pr in github_api_repo_prs_response]
    pulls = PullRequest.objects.filter(library=library, github_id__in=ids)
    assert pulls.exists()
    assert pulls.count() == expected_count

    # Test that the existing PR updated
    pull.refresh_from_db()
    assert pull.title == existing_pr_data.title
