# Copyright (C) 2021-2024  The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU General Public License version 3, or any later version
# See top-level LICENSE file for more information

import datetime
import functools
from itertools import permutations
from typing import Any, Dict, List
from unittest.mock import Mock

import attr
import pytest

from swh.model.hashutil import hash_to_bytes
from swh.scheduler.journal_client import (
    from_position_offset_to_days,
    max_date,
    next_visit_queue_position,
    process_journal_objects,
)
from swh.scheduler.model import LastVisitStatus, ListedOrigin, OriginVisitStats
from swh.scheduler.utils import utcnow


def test_journal_client_origin_visit_status_from_journal_fail(swh_scheduler):
    process_fn = functools.partial(
        process_journal_objects,
        scheduler=swh_scheduler,
    )

    with pytest.raises(AssertionError, match="Got unexpected origin_visit"):
        process_fn(
            {
                "origin_visit": [
                    {"url": "http://foobar.baz"},
                ]
            }
        )

    with pytest.raises(AssertionError, match="Expected origin_visit_status"):
        process_fn({})


ONE_DAY = datetime.timedelta(days=1)
ONE_YEAR = datetime.timedelta(days=366)

DATE3 = utcnow()
DATE2 = DATE3 - ONE_DAY
DATE1 = DATE2 - ONE_DAY


assert DATE1 < DATE2 < DATE3


@pytest.mark.parametrize(
    "dates,expected_max_date",
    [
        ((DATE1,), DATE1),
        ((None, DATE2), DATE2),
        ((DATE1, None), DATE1),
        ((DATE1, DATE2), DATE2),
        ((DATE2, DATE1), DATE2),
        ((DATE1, DATE2, DATE3), DATE3),
        ((None, DATE2, DATE3), DATE3),
        ((None, None, DATE3), DATE3),
        ((DATE1, None, DATE3), DATE3),
    ],
)
def test_max_date(dates, expected_max_date):
    assert max_date(*dates) == expected_max_date


def test_max_date_raise():
    with pytest.raises(ValueError, match="valid datetime"):
        max_date()
    with pytest.raises(ValueError, match="valid datetime"):
        max_date(None)
    with pytest.raises(ValueError, match="valid datetime"):
        max_date(None, None)


def test_journal_client_origin_visit_status_from_journal_ignored_status(swh_scheduler):
    """Only final statuses (full, partial) are important, the rest remain ignored."""
    # Trace method calls on the swh_scheduler
    swh_scheduler = Mock(wraps=swh_scheduler)

    visit_statuses = [
        {
            "origin": "foo",
            "visit": 1,
            "status": "created",
            "date": utcnow(),
            "type": "git",
            "snapshot": None,
        },
        {
            "origin": "bar",
            "visit": 1,
            "status": "ongoing",
            "date": utcnow(),
            "type": "svn",
            "snapshot": None,
        },
    ]

    process_journal_objects(
        {"origin_visit_status": visit_statuses}, scheduler=swh_scheduler
    )

    # All messages have been ignored: no stats have been upserted
    swh_scheduler.origin_visit_stats_upsert.assert_not_called()


def test_journal_client_ignore_missing_type(swh_scheduler):
    """Ignore statuses with missing type key"""
    # Trace method calls on the swh_scheduler
    swh_scheduler = Mock(wraps=swh_scheduler)

    date = utcnow()
    snapshot = hash_to_bytes("dddcc0710eb6cf9efd5b920a8453e1e07157bddd")
    visit_statuses = [
        {
            "origin": "foo",
            "visit": 1,
            "status": "full",
            "date": date,
            "snapshot": snapshot,
        },
    ]

    process_journal_objects(
        {"origin_visit_status": visit_statuses}, scheduler=swh_scheduler
    )

    # The message has been ignored: no stats have been upserted
    swh_scheduler.origin_visit_stats_upsert.assert_not_called()


def assert_visit_stats_ok(
    actual_visit_stats: OriginVisitStats,
    expected_visit_stats: OriginVisitStats,
    ignore_fields: List[str] = ["next_visit_queue_position"],
):
    """Utility test function to ensure visits stats read from the backend are in the right
    shape. The comparison on the next_visit_queue_position will be dealt with in
    dedicated tests so it's not tested in tests that are calling this function.

    """
    fields = attr.fields_dict(OriginVisitStats)
    defaults: Dict[str, Any] = {field: fields[field].default for field in ignore_fields}

    actual_visit_stats = attr.evolve(actual_visit_stats, **defaults)
    assert actual_visit_stats == expected_visit_stats


def test_journal_client_origin_visit_status_from_journal_last_not_found(swh_scheduler):
    visit_status = {
        "origin": "foo",
        "visit": 1,
        "status": "not_found",
        "date": DATE1,
        "type": "git",
        "snapshot": None,
    }

    process_journal_objects(
        {"origin_visit_status": [visit_status]}, scheduler=swh_scheduler
    )

    actual_origin_visit_stats = swh_scheduler.origin_visit_stats_get([("foo", "git")])
    assert_visit_stats_ok(
        actual_origin_visit_stats[0],
        OriginVisitStats(
            url="foo",
            visit_type="git",
            last_visit=visit_status["date"],
            last_visit_status=LastVisitStatus.not_found,
            next_position_offset=4,
            successive_visits=1,
        ),
    )

    visit_statuses = [
        {
            "origin": "foo",
            "visit": 3,
            "status": "not_found",
            "date": DATE2,
            "type": "git",
            "snapshot": None,
        },
        {
            "origin": "foo",
            "visit": 4,
            "status": "not_found",
            "date": DATE3,
            "type": "git",
            "snapshot": None,
        },
    ]

    process_journal_objects(
        {"origin_visit_status": visit_statuses}, scheduler=swh_scheduler
    )

    actual_origin_visit_stats = swh_scheduler.origin_visit_stats_get([("foo", "git")])
    assert_visit_stats_ok(
        actual_origin_visit_stats[0],
        OriginVisitStats(
            url="foo",
            visit_type="git",
            last_visit=DATE3,
            last_visit_status=LastVisitStatus.not_found,
            next_position_offset=6,
            successive_visits=3,
        ),
    )


def test_journal_client_origin_visit_status_from_journal_last_failed(swh_scheduler):
    visit_statuses = [
        {
            "origin": "foo",
            "visit": 1,
            "status": "partial",
            "date": utcnow(),
            "type": "git",
            "snapshot": None,
        },
        {
            "origin": "bar",
            "visit": 1,
            "status": "full",
            "date": DATE1,
            "type": "git",
            "snapshot": None,
        },
        {
            "origin": "bar",
            "visit": 2,
            "status": "full",
            "date": DATE2,
            "type": "git",
            "snapshot": None,
        },
        {
            "origin": "bar",
            "visit": 3,
            "status": "full",
            "date": DATE3,
            "type": "git",
            "snapshot": None,
        },
    ]

    process_journal_objects(
        {"origin_visit_status": visit_statuses}, scheduler=swh_scheduler
    )

    actual_origin_visit_stats = swh_scheduler.origin_visit_stats_get([("bar", "git")])
    assert_visit_stats_ok(
        actual_origin_visit_stats[0],
        OriginVisitStats(
            url="bar",
            visit_type="git",
            last_visit=DATE3,
            last_visit_status=LastVisitStatus.failed,
            next_position_offset=6,
            successive_visits=3,
        ),
    )


def test_journal_client_origin_visit_status_from_journal_last_failed2(swh_scheduler):
    visit_statuses = [
        {
            "origin": "bar",
            "visit": 2,
            "status": "failed",
            "date": DATE1,
            "type": "git",
            "snapshot": hash_to_bytes("d81cc0710eb6cf9efd5b920a8453e1e07157b6cd"),
        },
        {
            "origin": "bar",
            "visit": 3,
            "status": "failed",
            "date": DATE2,
            "type": "git",
            "snapshot": None,
        },
    ]

    process_journal_objects(
        {"origin_visit_status": visit_statuses}, scheduler=swh_scheduler
    )

    actual_origin_visit_stats = swh_scheduler.origin_visit_stats_get([("bar", "git")])
    assert_visit_stats_ok(
        actual_origin_visit_stats[0],
        OriginVisitStats(
            url="bar",
            visit_type="git",
            last_visit=DATE2,
            last_visit_status=LastVisitStatus.failed,
            next_position_offset=5,
            successive_visits=2,
        ),
    )


def test_journal_client_origin_visit_status_from_journal_last_successful(swh_scheduler):
    visit_statuses = [
        {
            "origin": "bar",
            "visit": 1,
            "status": "partial",
            "date": utcnow(),
            "type": "git",
            "snapshot": hash_to_bytes("d81cc0710eb6cf9efd5b920a8453e1e07157b6cd"),
        },
        {
            "origin": "foo",
            "visit": 1,
            "status": "full",
            "date": DATE1,
            "type": "git",
            "snapshot": hash_to_bytes("eeecc0710eb6cf9efd5b920a8453e1e07157bfff"),
        },
        {
            "origin": "foo",
            "visit": 2,
            "status": "partial",
            "date": DATE2,
            "type": "git",
            "snapshot": hash_to_bytes("aaacc0710eb6cf9efd5b920a8453e1e07157baaa"),
        },
        {
            "origin": "foo",
            "visit": 3,
            "status": "full",
            "date": DATE3,
            "type": "git",
            "snapshot": hash_to_bytes("dddcc0710eb6cf9efd5b920a8453e1e07157bddd"),
        },
    ]

    process_journal_objects(
        {"origin_visit_status": visit_statuses}, scheduler=swh_scheduler
    )

    actual_origin_visit_stats = swh_scheduler.origin_visit_stats_get([("foo", "git")])
    assert_visit_stats_ok(
        actual_origin_visit_stats[0],
        OriginVisitStats(
            url="foo",
            visit_type="git",
            last_successful=DATE3,
            last_visit=DATE3,
            last_visit_status=LastVisitStatus.successful,
            last_snapshot=hash_to_bytes("dddcc0710eb6cf9efd5b920a8453e1e07157bddd"),
            next_position_offset=0,
            successive_visits=3,
        ),
    )


def test_journal_client_origin_visit_status_from_journal_last_uneventful(swh_scheduler):
    visit_status = {
        "origin": "foo",
        "visit": 1,
        "status": "full",
        "date": DATE3 + ONE_DAY,
        "type": "git",
        "snapshot": hash_to_bytes("d81cc0710eb6cf9efd5b920a8453e1e07157b6cd"),
    }

    # Let's insert some visit stats with some previous visit information
    swh_scheduler.origin_visit_stats_upsert(
        [
            OriginVisitStats(
                url=visit_status["origin"],
                visit_type=visit_status["type"],
                last_successful=DATE2,
                last_visit=DATE3,
                last_visit_status=LastVisitStatus.failed,
                last_snapshot=visit_status["snapshot"],
                next_visit_queue_position=None,
                next_position_offset=4,
                successive_visits=1,
            )
        ]
    )

    process_journal_objects(
        {"origin_visit_status": [visit_status]}, scheduler=swh_scheduler
    )

    actual_origin_visit_stats = swh_scheduler.origin_visit_stats_get(
        [(visit_status["origin"], visit_status["type"])]
    )

    assert_visit_stats_ok(
        actual_origin_visit_stats[0],
        OriginVisitStats(
            url=visit_status["origin"],
            visit_type=visit_status["type"],
            last_visit=DATE3 + ONE_DAY,
            last_successful=DATE3 + ONE_DAY,
            last_visit_status=LastVisitStatus.successful,
            last_snapshot=visit_status["snapshot"],
            next_visit_queue_position=None,
            next_position_offset=5,
            successive_visits=1,
        ),
    )


VISIT_STATUSES = [
    {**ovs, "date": DATE1 + n * ONE_DAY}
    for n, ovs in enumerate(
        [
            {
                "origin": "foo",
                "type": "git",
                "visit": 1,
                "status": "created",
                "snapshot": None,
            },
            {
                "origin": "foo",
                "type": "git",
                "visit": 1,
                "status": "full",
                "snapshot": hash_to_bytes("d81cc0710eb6cf9efd5b920a8453e1e07157b6cd"),
            },
            {
                "origin": "foo",
                "type": "git",
                "visit": 2,
                "status": "created",
                "snapshot": None,
            },
            {
                "origin": "foo",
                "type": "git",
                "visit": 2,
                "status": "full",
                "snapshot": hash_to_bytes("d81cc0710eb6cf9efd5b920a8453e1e07157b6cd"),
            },
        ]
    )
]


@pytest.mark.parametrize(
    "visit_statuses", permutations(VISIT_STATUSES, len(VISIT_STATUSES))
)
def test_journal_client_origin_visit_status_permutation0(visit_statuses, swh_scheduler):
    """Ensure out of order topic subscription ends up in the same final state"""
    process_journal_objects(
        {"origin_visit_status": visit_statuses}, scheduler=swh_scheduler
    )

    actual_origin_visit_stats = swh_scheduler.origin_visit_stats_get([("foo", "git")])
    visit_stats = actual_origin_visit_stats[0]
    assert_visit_stats_ok(
        visit_stats,
        OriginVisitStats(
            url="foo",
            visit_type="git",
            last_successful=DATE1 + 3 * ONE_DAY,
            last_visit=DATE1 + 3 * ONE_DAY,
            last_visit_status=LastVisitStatus.successful,
            last_snapshot=hash_to_bytes("d81cc0710eb6cf9efd5b920a8453e1e07157b6cd"),
        ),
        ignore_fields=[
            "next_visit_queue_position",
            "next_position_offset",
            "successive_visits",
        ],
    )

    # We ignore out of order messages, so the next_position_offset isn't exact
    # depending on the permutation. What matters is consistency of the final
    # dates (last_visit and last_successful).
    assert 4 <= visit_stats.next_position_offset <= 5
    # same goes for successive_visits
    assert 1 <= visit_stats.successive_visits <= 2


VISIT_STATUSES_1 = [
    {**ovs, "date": DATE1 + n * ONE_DAY}
    for n, ovs in enumerate(
        [
            {
                "origin": "cavabarder",
                "type": "hg",
                "visit": 1,
                "status": "partial",
                "snapshot": hash_to_bytes("d81cc0710eb6cf9efd5b920a8453e1e07157b6cd"),
            },
            {
                "origin": "cavabarder",
                "type": "hg",
                "visit": 2,
                "status": "full",
                "snapshot": hash_to_bytes("d81cc0710eb6cf9efd5b920a8453e1e07157b6cd"),
            },
            {
                "origin": "cavabarder",
                "type": "hg",
                "visit": 3,
                "status": "full",
                "snapshot": hash_to_bytes("aaaaaabbbeb6cf9efd5b920a8453e1e07157b6cd"),
            },
            {
                "origin": "cavabarder",
                "type": "hg",
                "visit": 4,
                "status": "full",
                "snapshot": hash_to_bytes("aaaaaabbbeb6cf9efd5b920a8453e1e07157b6cd"),
            },
        ]
    )
]


@pytest.mark.parametrize(
    "visit_statuses", permutations(VISIT_STATUSES_1, len(VISIT_STATUSES_1))
)
def test_journal_client_origin_visit_status_permutation1(visit_statuses, swh_scheduler):
    """Ensure out of order topic subscription ends up in the same final state"""
    process_journal_objects(
        {"origin_visit_status": visit_statuses}, scheduler=swh_scheduler
    )

    actual_visit_stats = swh_scheduler.origin_visit_stats_get([("cavabarder", "hg")])
    visit_stats = actual_visit_stats[0]

    assert_visit_stats_ok(
        visit_stats,
        OriginVisitStats(
            url="cavabarder",
            visit_type="hg",
            last_successful=DATE1 + 3 * ONE_DAY,
            last_visit=DATE1 + 3 * ONE_DAY,
            last_visit_status=LastVisitStatus.successful,
            last_snapshot=hash_to_bytes("aaaaaabbbeb6cf9efd5b920a8453e1e07157b6cd"),
        ),
        ignore_fields=[
            "next_visit_queue_position",
            "next_position_offset",
            "successive_visits",
        ],
    )

    # We ignore out of order messages, so the next_position_offset isn't exact
    # depending on the permutation. What matters is consistency of the final
    # dates (last_visit and last_successful).
    assert 2 <= visit_stats.next_position_offset <= 5
    # same goes for successive_visits
    assert 1 <= visit_stats.successive_visits <= 4


VISIT_STATUSES_2 = [
    {**ovs, "date": DATE1 + n * ONE_DAY}
    for n, ovs in enumerate(
        [
            {
                "origin": "cavabarder",
                "type": "hg",
                "visit": 1,
                "status": "full",
                "snapshot": hash_to_bytes("0000000000000000000000000000000000000000"),
            },
            {
                "origin": "cavabarder",
                "type": "hg",
                "visit": 2,
                "status": "full",
                "snapshot": hash_to_bytes("1111111111111111111111111111111111111111"),
            },
            {
                "origin": "iciaussi",
                "type": "hg",
                "visit": 1,
                "status": "full",
                "snapshot": hash_to_bytes("2222222222222222222222222222222222222222"),
            },
            {
                "origin": "iciaussi",
                "type": "hg",
                "visit": 2,
                "status": "full",
                "snapshot": hash_to_bytes("3333333333333333333333333333333333333333"),
            },
            {
                "origin": "cavabarder",
                "type": "git",
                "visit": 1,
                "status": "full",
                "snapshot": hash_to_bytes("4444444444444444444444444444444444444444"),
            },
            {
                "origin": "cavabarder",
                "type": "git",
                "visit": 2,
                "status": "full",
                "snapshot": hash_to_bytes("5555555555555555555555555555555555555555"),
            },
            {
                "origin": "iciaussi",
                "type": "git",
                "visit": 1,
                "status": "full",
                "snapshot": hash_to_bytes("6666666666666666666666666666666666666666"),
            },
            {
                "origin": "iciaussi",
                "type": "git",
                "visit": 2,
                "status": "full",
                "snapshot": hash_to_bytes("7777777777777777777777777777777777777777"),
            },
        ]
    )
]


def test_journal_client_origin_visit_status_after_grab_next_visits(
    swh_scheduler, stored_lister
):
    """Ensure OriginVisitStat entries created in the db as a result of calling
    grab_next_visits() do not mess the OriginVisitStats upsert mechanism.

    """

    listed_origins = [
        ListedOrigin(lister_id=stored_lister.id, url=url, visit_type=visit_type)
        for (url, visit_type) in set((v["origin"], v["type"]) for v in VISIT_STATUSES_2)
    ]
    swh_scheduler.record_listed_origins(listed_origins)
    before = utcnow()
    swh_scheduler.grab_next_visits(
        visit_type="git", count=10, policy="oldest_scheduled_first"
    )
    after = utcnow()

    assert swh_scheduler.origin_visit_stats_get([("cavabarder", "hg")]) == []
    assert swh_scheduler.origin_visit_stats_get([("cavabarder", "git")])[0] is not None

    process_journal_objects(
        {"origin_visit_status": VISIT_STATUSES_2}, scheduler=swh_scheduler
    )

    for url in ("cavabarder", "iciaussi"):
        ovs = swh_scheduler.origin_visit_stats_get([(url, "git")])[0]
        assert before <= ovs.last_scheduled <= after

        ovs = swh_scheduler.origin_visit_stats_get([(url, "hg")])[0]
        assert ovs.last_scheduled is None

    ovs = swh_scheduler.origin_visit_stats_get([("cavabarder", "git")])[0]
    assert ovs.last_successful == DATE1 + 5 * ONE_DAY
    assert ovs.last_visit == DATE1 + 5 * ONE_DAY
    assert ovs.last_visit_status == LastVisitStatus.successful
    assert ovs.last_snapshot == hash_to_bytes(
        "5555555555555555555555555555555555555555"
    )


def test_journal_client_origin_visit_status_duplicated_messages(swh_scheduler):
    """A duplicated message must be ignored"""
    visit_status = {
        "origin": "foo",
        "visit": 1,
        "status": "full",
        "date": DATE1,
        "type": "git",
        "snapshot": hash_to_bytes("aaaaaabbbeb6cf9efd5b920a8453e1e07157b6cd"),
    }

    process_journal_objects(
        {"origin_visit_status": [visit_status]}, scheduler=swh_scheduler
    )

    process_journal_objects(
        {"origin_visit_status": [visit_status]}, scheduler=swh_scheduler
    )

    actual_origin_visit_stats = swh_scheduler.origin_visit_stats_get([("foo", "git")])
    assert_visit_stats_ok(
        actual_origin_visit_stats[0],
        OriginVisitStats(
            url="foo",
            visit_type="git",
            last_successful=DATE1,
            last_visit=DATE1,
            last_visit_status=LastVisitStatus.successful,
            last_snapshot=hash_to_bytes("aaaaaabbbeb6cf9efd5b920a8453e1e07157b6cd"),
            successive_visits=1,
        ),
    )


def test_journal_client_origin_visit_status_several_upsert(swh_scheduler):
    """An old message updates old information"""
    visit_status1 = {
        "origin": "foo",
        "visit": 1,
        "status": "full",
        "date": DATE1,
        "type": "git",
        "snapshot": hash_to_bytes("aaaaaabbbeb6cf9efd5b920a8453e1e07157b6cd"),
    }

    visit_status2 = {
        "origin": "foo",
        "visit": 1,
        "status": "full",
        "date": DATE2,
        "type": "git",
        "snapshot": hash_to_bytes("aaaaaabbbeb6cf9efd5b920a8453e1e07157b6cd"),
    }

    process_journal_objects(
        {"origin_visit_status": [visit_status2]}, scheduler=swh_scheduler
    )

    process_journal_objects(
        {"origin_visit_status": [visit_status1]}, scheduler=swh_scheduler
    )

    actual_origin_visit_stats = swh_scheduler.origin_visit_stats_get([("foo", "git")])
    assert_visit_stats_ok(
        actual_origin_visit_stats[0],
        OriginVisitStats(
            url="foo",
            visit_type="git",
            last_successful=DATE2,
            last_visit=DATE2,
            last_visit_status=LastVisitStatus.successful,
            last_snapshot=hash_to_bytes("aaaaaabbbeb6cf9efd5b920a8453e1e07157b6cd"),
            next_position_offset=4,
            successive_visits=1,
        ),
    )


VISIT_STATUSES_SAME_SNAPSHOT = [
    {**ovs, "date": DATE1 + n * ONE_YEAR}
    for n, ovs in enumerate(
        [
            {
                "origin": "cavabarder",
                "type": "hg",
                "visit": 3,
                "status": "full",
                "snapshot": hash_to_bytes("aaaaaabbbeb6cf9efd5b920a8453e1e07157b6cd"),
            },
            {
                "origin": "cavabarder",
                "type": "hg",
                "visit": 4,
                "status": "full",
                "snapshot": hash_to_bytes("aaaaaabbbeb6cf9efd5b920a8453e1e07157b6cd"),
            },
            {
                "origin": "cavabarder",
                "type": "hg",
                "visit": 4,
                "status": "full",
                "snapshot": hash_to_bytes("aaaaaabbbeb6cf9efd5b920a8453e1e07157b6cd"),
            },
        ]
    )
]


@pytest.mark.parametrize(
    "visit_statuses",
    permutations(VISIT_STATUSES_SAME_SNAPSHOT, len(VISIT_STATUSES_SAME_SNAPSHOT)),
)
def test_journal_client_origin_visit_statuses_same_snapshot_permutation(
    visit_statuses, swh_scheduler
):
    """Ensure out of order topic subscription ends up in the same final state"""
    process_journal_objects(
        {"origin_visit_status": visit_statuses}, scheduler=swh_scheduler
    )

    actual_origin_visit_stats = swh_scheduler.origin_visit_stats_get(
        [("cavabarder", "hg")]
    )
    visit_stats = actual_origin_visit_stats[0]
    assert_visit_stats_ok(
        visit_stats,
        OriginVisitStats(
            url="cavabarder",
            visit_type="hg",
            last_successful=DATE1 + 2 * ONE_YEAR,
            last_visit=DATE1 + 2 * ONE_YEAR,
            last_visit_status=LastVisitStatus.successful,
            last_snapshot=hash_to_bytes("aaaaaabbbeb6cf9efd5b920a8453e1e07157b6cd"),
        ),
        ignore_fields=[
            "next_visit_queue_position",
            "next_position_offset",
            "successive_visits",
        ],
    )

    # We ignore out of order messages, so the next_position_offset isn't exact
    # depending on the permutation. What matters is consistency of the final
    # dates (last_visit and last_successful).
    assert 4 <= visit_stats.next_position_offset <= 6
    # same goes for successive_visits
    assert 1 <= visit_stats.successive_visits <= 3


@pytest.mark.parametrize(
    "position_offset, interval",
    [
        (0, 1),
        (1, 1),
        (2, 2),
        (3, 2),
        (4, 2),
        (5, 4),
        (6, 16),
        (7, 64),
        (8, 256),
        (9, 1024),
        (10, 4096),
        (11, 16384),
    ],
)
def test_journal_client_from_position_offset_to_days(position_offset, interval):
    assert from_position_offset_to_days(position_offset) == interval


def test_journal_client_from_position_offset_to_days_only_positive_input():
    with pytest.raises(AssertionError):
        from_position_offset_to_days(-1)


@pytest.mark.parametrize(
    "fudge_factor,next_position_offset",
    [
        (0.01, 1),
        (-0.01, 5),
        (0.1, 8),
        (-0.1, 10),
    ],
)
def test_next_visit_queue_position(mocker, fudge_factor, next_position_offset):
    mock_random = mocker.patch("swh.scheduler.journal_client.random.uniform")
    mock_random.return_value = fudge_factor

    actual_position = next_visit_queue_position(
        {},
        {
            "next_position_offset": next_position_offset,
            "visit_type": "svn",
        },
    )

    assert actual_position == int(
        24
        * 3600
        * from_position_offset_to_days(next_position_offset)
        * (1 + fudge_factor)
    )

    assert mock_random.called


@pytest.mark.parametrize(
    "fudge_factor,next_position_offset",
    [
        (0.02, 2),
        (-0.02, 3),
        (0, 7),
        (-0.09, 9),
    ],
)
def test_next_visit_queue_position_with_state(
    mocker, fudge_factor, next_position_offset
):
    mock_random = mocker.patch("swh.scheduler.journal_client.random.uniform")
    mock_random.return_value = fudge_factor

    actual_position = next_visit_queue_position(
        {"git": 0},
        {
            "next_position_offset": next_position_offset,
            "visit_type": "git",
        },
    )

    assert actual_position == int(
        24
        * 3600
        * from_position_offset_to_days(next_position_offset)
        * (1 + fudge_factor)
    )

    assert mock_random.called


@pytest.mark.parametrize(
    "fudge_factor,next_position_offset",
    [
        (0.03, 3),
        (-0.03, 4),
        (0.08, 7),
        (-0.08, 9),
    ],
)
def test_next_visit_queue_position_with_next_visit_queue(
    mocker, fudge_factor, next_position_offset
):
    mock_random = mocker.patch("swh.scheduler.journal_client.random.uniform")
    mock_random.return_value = fudge_factor

    actual_position = next_visit_queue_position(
        {},
        {
            "next_position_offset": next_position_offset,
            "visit_type": "hg",
            "next_visit_queue_position": 0,
        },
    )

    assert actual_position == int(
        24
        * 3600
        * from_position_offset_to_days(next_position_offset)
        * (1 + fudge_factor)
    )

    assert mock_random.called


def test_disable_failing_origins(swh_scheduler):
    """Origin with too many failed attempts ends up being deactivated in the scheduler."""

    # actually store the origin in the scheduler so we can check it's deactivated in the
    # end.
    lister = swh_scheduler.get_or_create_lister(
        name="something", instance_name="something"
    )
    origin = ListedOrigin(
        url="bar", enabled=True, visit_type="svn", lister_id=lister.id
    )
    swh_scheduler.record_listed_origins([origin])

    visit_statuses = [
        {
            "origin": "bar",
            "visit": 2,
            "status": "failed",
            "date": DATE1,
            "type": "svn",
            "snapshot": None,
        },
        {
            "origin": "bar",
            "visit": 3,
            "status": "failed",
            "date": DATE2,
            "type": "svn",
            "snapshot": None,
        },
        {
            "origin": "bar",
            "visit": 3,
            "status": "failed",
            "date": DATE3,
            "type": "svn",
            "snapshot": None,
        },
    ]

    process_journal_objects(
        {"origin_visit_status": visit_statuses}, scheduler=swh_scheduler
    )

    actual_origin_visit_stats = swh_scheduler.origin_visit_stats_get([("bar", "svn")])
    assert_visit_stats_ok(
        actual_origin_visit_stats[0],
        OriginVisitStats(
            url="bar",
            visit_type="svn",
            last_successful=None,
            last_visit=DATE3,
            last_visit_status=LastVisitStatus.failed,
            next_position_offset=6,
            successive_visits=3,
        ),
    )

    # Now check that the origin in question is disabled
    actual_page = swh_scheduler.get_listed_origins(url="bar", enabled=False)

    assert len(actual_page.results) == 1
    assert actual_page.next_page_token is None

    for origin in actual_page.results:
        assert origin.enabled is False
        assert origin.lister_id == lister.id
        assert origin.url == "bar"
        assert origin.visit_type == "svn"
