from maps.wikimap.stat.libs.tracker.lib.issues import (
    _UNKNOWN_COMPONENT,
    _get_all_changes,
    _timestamp_to_date,
    _get_issues_closed_at,
    _get_issues_with_paid_resolutions,
    _get_issues_created_at,
    _add_components,
)
from maps.wikimap.stat.libs.tracker import (
    filter_by_queues,
    get_closed_issues,
    get_created_issues,
    check_issues,
)
from maps.wikimap.stat.libs import nile_ut

from nile.api.v1 import (
    Record,
    clusters,
    local
)
from yt.yson import to_yson_type

import datetime
import pytest


def timestamp(datetime_iso_str):
    return datetime.datetime.fromisoformat(datetime_iso_str).timestamp() * 1000


def mock_table(job, label):
    return job.table('//path/to/a/mock/table').label(label)


@pytest.fixture
def job():
    return clusters.MockCluster().job()


def test_should_filter_by_queues():
    result = nile_ut.yt_run(
        filter_by_queues,
        issues=nile_ut.Table([
            Record(key=b'FEEDBACKPW-1', test_column=1),
            Record(key=b'MAPSPW-2',     test_column=2),
            Record(key=b'OTHER-3',      test_column=3),
            Record(key=b'OUTKARTPW-4',  test_column=4),
        ]),
        queues=frozenset({'FEEDBACKPW', 'MAPSPW', 'OUTKARTPW'}),
    )

    assert sorted([
        Record(key=b'FEEDBACKPW-1', test_column=1),
        Record(key=b'MAPSPW-2',     test_column=2),
        Record(key=b'OUTKARTPW-4',  test_column=4),
    ]) == sorted(result)


def test_should_get_all_changes(job):
    result = []

    _get_all_changes(
        mock_table(job, 'issue_events')
    ).label('result')

    job.local_run(
        sources={
            'issue_events': local.StreamSource([
                Record(issue=b'issue id 1', date=1, changes=to_yson_type([{b'field': b'field 1',  b'newValue': {b'value': None}}])),
                Record(issue=b'issue id 2', date=2, changes=to_yson_type([{b'field': b'field 2',  b'newValue': {b'value': b'value 2'}}])),
                Record(issue=b'issue id 3', date=3, changes=to_yson_type([{b'field': b'field 31', b'newValue': {b'value': b'value 31'}},
                                                                          {b'field': b'field 32', b'newValue': {b'value': b'value 32'}}])),
            ])
        },
        sinks={'result': local.ListSink(result)}
    )

    assert sorted([
        Record(issue_id='issue id 1', date=1, field='field 1', value=None),
        Record(issue_id='issue id 2', date=2, field='field 2', value=b'value 2'),
        Record(issue_id='issue id 3', date=3, field='field 31', value=b'value 31'),
        Record(issue_id='issue id 3', date=3, field='field 32', value=b'value 32'),
    ]) == sorted(result)


def test_timestamp_to_date():
    assert _timestamp_to_date(timestamp('2020-01-25')) == '2020-01-25'
    assert _timestamp_to_date(timestamp('2020-01-26T00:00:00')) == '2020-01-26'
    assert _timestamp_to_date(timestamp('2020-01-27T23:59:59')) == '2020-01-27'


def test_should_get_closed_statuses(job):
    result = []

    _get_issues_closed_at(
        {'2020-01-25'},
        mock_table(job, 'all_changes'),
        mock_table(job, 'statuses')
    )

    job.local_run(
        sources={
            'statuses': local.StreamSource([
                Record(id=b'status id 1', key=b'closed', test_field=b'other 1'),
                Record(id=b'status id 2', key=b'open',   test_field=b'other 2'),
                Record(id=b'status id 3', key=b'closed', test_field=b'other 3'),
            ])
        },
        sinks={'closed_statuses': local.ListSink(result)}
    )

    assert sorted([
        Record(status_id=b'status id 1'),
        Record(status_id=b'status id 3'),
    ]) == sorted(result)


def test_should_get_statuses_changes(job):
    result = []

    _get_issues_closed_at(
        {'2020-01-25'},
        mock_table(job, 'all_changes'),
        mock_table(job, 'statuses')
    )

    job.local_run(
        sources={
            'all_changes': local.StreamSource([
                Record(issue_id=b'issue id 1', date=timestamp('2020-01-25'), field=b'status', value=to_yson_type({b'id': b'status id 1'})),
                Record(issue_id=b'issue id 2', date=timestamp('2020-01-25'), field=b'other',  value=to_yson_type({b'id': b'status id 2'})),
                Record(issue_id=b'issue id 1', date=timestamp('2020-01-25'), field=b'other',  value=to_yson_type({b'id': b'status id 3'})),
                Record(issue_id=b'issue id 1', date=timestamp('2020-01-25'), field=b'status', value=to_yson_type({b'id': b'status id 4'})),
                Record(issue_id=b'issue id 3', date=timestamp('2020-01-25'), field=b'status', value=to_yson_type({b'id': b'status id 5'})),
            ])
        },
        sinks={'statuses_changes': local.ListSink(result)}
    )

    assert sorted([
        Record(issue_id=b'issue id 1', date=timestamp('2020-01-25'), status_id='status id 1'),
        Record(issue_id=b'issue id 1', date=timestamp('2020-01-25'), status_id='status id 4'),
        Record(issue_id=b'issue id 3', date=timestamp('2020-01-25'), status_id='status id 5'),
    ]) == sorted(result)


def test_should_get_closed_changes(job):
    result = []

    _get_issues_closed_at(
        {'2020-01-25'},
        mock_table(job, 'all_changes'),
        mock_table(job, 'statuses')
    )

    job.local_run(
        sources={
            'statuses_changes': local.StreamSource([
                Record(issue_id=b'issue id 1', date=timestamp('2020-01-25'), status_id=b'closed status id'),
                Record(issue_id=b'issue id 2', date=timestamp('2020-01-26'), status_id=b'other status id'),
                Record(issue_id=b'issue id 1', date=timestamp('2020-01-27'), status_id=b'closed status id'),
                Record(issue_id=b'issue id 2', date=timestamp('2020-01-28'), status_id=b'closed status id'),
            ]),
            'closed_statuses': local.StreamSource([
                Record(status_id=b'closed status id'),
            ])
        },
        sinks={'closed_changes': local.ListSink(result)}
    )

    assert sorted([
        Record(issue_id=b'issue id 1', date=timestamp('2020-01-25')),
        Record(issue_id=b'issue id 1', date=timestamp('2020-01-27')),
        Record(issue_id=b'issue id 2', date=timestamp('2020-01-28')),
    ]) == sorted(result)


def test_should_get_first_closed_changes(job):
    result = []

    _get_issues_closed_at(
        {'2020-01-25'},
        mock_table(job, 'all_changes'),
        mock_table(job, 'statuses')
    )

    job.local_run(
        sources={
            'closed_changes': local.StreamSource([
                Record(issue_id=b'issue id 1', date=timestamp('2020-01-25')),
                Record(issue_id=b'issue id 2', date=timestamp('2020-01-26')),
                Record(issue_id=b'issue id 1', date=timestamp('2020-01-24')),
                Record(issue_id=b'issue id 2', date=timestamp('2020-01-25')),
                Record(issue_id=b'issue id 1', date=timestamp('2020-01-26')),
            ])},
        sinks={'first_closed_changes': local.ListSink(result)}
    )

    assert sorted([
        Record(issue_id=b'issue id 1', date=timestamp('2020-01-24')),
        Record(issue_id=b'issue id 2', date=timestamp('2020-01-25')),
    ]) == sorted(result)


def test_should_get_issues_closed_at_from_first_closed_changes(job):
    result = []

    _get_issues_closed_at(
        {'2020-01-25'},
        mock_table(job, 'all_changes'),
        mock_table(job, 'statuses')
    ).label('result')

    job.local_run(
        sources={
            'first_closed_changes': local.StreamSource([
                Record(issue_id=b'issue id 1', date=timestamp('2020-01-24')),
                Record(issue_id=b'issue id 2', date=timestamp('2020-01-25')),
                Record(issue_id=b'issue id 3', date=timestamp('2020-01-26')),
            ])
        },
        sinks={'result': local.ListSink(result)}
    )

    assert sorted([
        Record(issue_id=b'issue id 2', date='2020-01-25'),
    ]) == sorted(result)


def test_should_get_issues_closed_at(job):
    result = []

    _get_issues_closed_at(
        {'2020-01-25'},
        mock_table(job, 'all_changes'),
        mock_table(job, 'statuses')
    ).label('result')

    job.local_run(
        sources={
            'all_changes': local.StreamSource([
                # Okay, but has an additional closed status after date of interest
                Record(issue_id=b'issue id 1', date=timestamp('2020-01-24'), field=b'status', value=to_yson_type({b'id': b'status id open'})),
                Record(issue_id=b'issue id 1', date=timestamp('2020-01-25'), field=b'status', value=to_yson_type({b'id': b'status id closed'})),
                Record(issue_id=b'issue id 1', date=timestamp('2020-01-26'), field=b'status', value=to_yson_type({b'id': b'status id closed'})),

                # Wrong field
                Record(issue_id=b'issue id 2', date=timestamp('2020-01-25'), field=b'other',  value=to_yson_type({b'id': b'status id closed'})),

                # First closed status is before date of interest
                Record(issue_id=b'issue id 3', date=timestamp('2020-01-24'), field=b'status', value=to_yson_type({b'id': b'status id closed'})),
                Record(issue_id=b'issue id 3', date=timestamp('2020-01-25'), field=b'status', value=to_yson_type({b'id': b'status id closed'})),

                # Okay
                Record(issue_id=b'issue id 4', date=timestamp('2020-01-25'), field=b'status', value=to_yson_type({b'id': b'status id closed'})),
            ]),
            'statuses': local.StreamSource([
                Record(id='status id closed', key=b'closed'),
                Record(id='status id open', key=b'open'),
            ])
        },
        sinks={'result': local.ListSink(result)}
    )

    assert sorted([
        Record(issue_id=b'issue id 1', date='2020-01-25'),
        Record(issue_id=b'issue id 4', date='2020-01-25'),
    ]) == sorted(result)


def test_should_get_paid_resolutions(job):
    result = []

    _get_issues_with_paid_resolutions(
        mock_table(job, 'issues_closed_at_date'),
        mock_table(job, 'issues'),
        mock_table(job, 'resolutions')
    )

    job.local_run(
        sources={
            'resolutions': local.StreamSource([
                Record(id=b'resolution id 1', key=b'fixed', test_column=1),
                Record(id=b'resolution id 2', key=b'other', test_column=2),
            ])
        },
        sinks={'paid_resolutions': local.ListSink(result)}
    )

    assert sorted([
        Record(resolution_id=b'resolution id 1'),
    ]) == sorted(result)


def test_should_get_issues_with_paid_resolutions(job):
    result = []

    _get_issues_with_paid_resolutions(
        mock_table(job, 'issues_closed_at_date'),
        mock_table(job, 'issues'),
        mock_table(job, 'resolutions')
    ).label('result')

    job.local_run(
        sources={
            'issues_closed_at_date': local.StreamSource([
                Record(issue_id=b'issue id 1', date=b'2020-01-25'),
                Record(issue_id=b'issue id 2', date=b'2020-01-25'),
                Record(issue_id=b'issue id 4', date=b'2020-01-25'),
            ]),
            'issues': local.StreamSource([
                Record(id=b'issue id 1', key=b'ticket 1', resolution=b'resolution id fixed',   assignee=1, components=1, storyPoints=1),  # Ok
                Record(id=b'issue id 2', key=b'ticket 2', resolution=b'resolution id other',   assignee=2, components=2, storyPoints=2),  # Non-paid resolution
                Record(id=b'issue id 3', key=b'ticket 3', resolution=b'resolution id fixed',   assignee=3, components=3, storyPoints=3),  # Not in closed_at_date
                Record(id=b'issue id 4', key=b'ticket 4', resolution=b'resolution id unknown', assignee=4, components=4, storyPoints=4),  # Non-paid unknown resolution
            ]),
            'resolutions': local.StreamSource([
                Record(id=b'resolution id fixed', key=b'fixed'),
                Record(id=b'resolution id other', key=b'other'),
            ])
        },
        sinks={'result': local.ListSink(result)}
    )

    assert sorted([
        Record(staff_uid=1, components=1, ticket=b'ticket 1', story_points=1, date=b'2020-01-25'),
    ]) == sorted(result)


def test_should_get_issues_created_at(job):
    result = []

    _get_issues_created_at(
        {'2020-01-25'},
        mock_table(job, 'issues'),
    ).label('result')

    job.local_run(
        sources={
            'issues': local.StreamSource([
                Record(id=b'issue id 1', key=b'ticket 1', author=1, components=1, created=timestamp('2020-01-24 12:00:00')),
                Record(id=b'issue id 2', key=b'ticket 2', author=2, components=2, created=timestamp('2020-01-24 23:59:59')),

                Record(id=b'issue id 3', key=b'ticket 3', author=3, components=3, created=timestamp('2020-01-25 00:00:00')),
                Record(id=b'issue id 4', key=b'ticket 4', author=4, components=4, created=timestamp('2020-01-25 12:00:00')),
                Record(id=b'issue id 5', key=b'ticket 5', author=5, components=5, created=timestamp('2020-01-25 23:59:59')),

                Record(id=b'issue id 6', key=b'ticket 6', author=6, components=6, created=timestamp('2020-01-26 00:00:00')),
                Record(id=b'issue id 7', key=b'ticket 7', author=7, components=7, created=timestamp('2020-01-26 12:00:00')),

            ]),
        },
        sinks={'result': local.ListSink(result)}
    )

    assert sorted([
        Record(staff_uid=3, components=3, ticket=b'ticket 3', date='2020-01-25'),
        Record(staff_uid=4, components=4, ticket=b'ticket 4', date='2020-01-25'),
        Record(staff_uid=5, components=5, ticket=b'ticket 5', date='2020-01-25'),
    ]) == sorted(result)


def test_should_add_components(job):
    result = []

    _add_components(
        mock_table(job, 'issues'),
        mock_table(job, 'components')
    ).label('result')

    job.local_run(
        sources={
            'issues': local.StreamSource([
                Record(components=to_yson_type([b'component id 1']), test_column=1),
                Record(components=to_yson_type([b'component id 2']), test_column=2),
                Record(components=to_yson_type([b'component id 4']), test_column=4),
            ]),
            'components': local.StreamSource([
                Record(id='component id 1', name=b'component 1'),
                Record(id='component id 2', name=b'component 2'),
                Record(id='component id 3', name=b'component 3'),
                Record(id='component id 4', name=b'component 4'),
            ])
        },
        sinks={'result': local.ListSink(result)}
    )

    assert sorted([
        Record(component=b'component 1', test_column=1),
        Record(component=b'component 2', test_column=2),
        Record(component=b'component 4', test_column=4),
    ]) == sorted(result)


def test_should_add_unknown_component_for_wrong_components_amount(job):
    result = []

    _add_components(
        mock_table(job, 'issues'),
        mock_table(job, 'components')
    ).label('result')

    job.local_run(
        sources={
            'issues': local.StreamSource([
                Record(components=to_yson_type([]), test_column=1),
                Record(components=to_yson_type([b'component id 21', b'component id 22']), test_column=2),
            ]),
            'components': local.StreamSource([
                Record(id='component id 21', name=b'component 21'),
                Record(id='component id 22', name=b'component 22'),
            ])
        },
        sinks={'result': local.ListSink(result)}
    )

    assert sorted([
        Record(component=_UNKNOWN_COMPONENT, test_column=1),
        Record(component=_UNKNOWN_COMPONENT, test_column=2),
    ]) == sorted(result)


def test_should_get_closed_issues(job):
    result = []

    get_closed_issues(
        {'2020-01-25'},
        mock_table(job, 'components'),
        mock_table(job, 'issue_events'),
        mock_table(job, 'issues'),
        mock_table(job, 'resolutions'),
        mock_table(job, 'statuses')
    ).label('result')

    open_changes = to_yson_type([{b'field': b'status', b'newValue': {b'value': {b'id': b'status id open'}}}])
    closed_changes = to_yson_type([{b'field': b'status', b'newValue': {b'value': {b'id': b'status id closed'}}}])

    job.local_run(
        sources={
            'components': local.StreamSource([
                Record(id='component id 1', name=b'component 1'),
                Record(id='component id 2', name=b'component 2'),
                Record(id='component id 3', name=b'component 3'),
                Record(id='component id 4', name=b'component 4'),
                Record(id='component id 5', name=b'component 5'),
                Record(id='component id 6', name=b'component 6'),  # Unused entry
            ]),
            'issue_events': local.StreamSource([
                # First closed at another day
                Record(issue=b'issue id 1', date=timestamp('2020-01-23'), changes=closed_changes),
                Record(issue=b'issue id 1', date=timestamp('2020-01-24'), changes=open_changes),
                Record(issue=b'issue id 1', date=timestamp('2020-01-25'), changes=closed_changes),

                # Non-closed
                Record(issue=b'issue id 2', date=timestamp('2020-01-25'), changes=open_changes),

                # Okay, but reopened later
                Record(issue=b'issue id 3', date=timestamp('2020-01-25'), changes=closed_changes),
                Record(issue=b'issue id 3', date=timestamp('2020-01-26'), changes=open_changes),

                # Okay, but without linked account
                Record(issue=b'issue id 4', date=timestamp('2020-01-25'), changes=closed_changes),

                # Unpaid resolution
                Record(issue=b'issue id 5', date=timestamp('2020-01-25'), changes=closed_changes),
            ]),
            'issues': local.StreamSource([
                Record(id='issue id 1', resolution=b'resolution id fixed', assignee=1, components=to_yson_type([b'component id 1']), key=b'queue1-1', storyPoints=1),
                Record(id='issue id 2', resolution=b'resolution id fixed', assignee=2, components=to_yson_type([b'component id 2']), key=b'queue2-2', storyPoints=2),
                Record(id='issue id 3', resolution=b'resolution id fixed', assignee=3, components=to_yson_type([b'component id 3']), key=b'queue3-3', storyPoints=3),
                Record(id='issue id 4', resolution=b'resolution id fixed', assignee=4, components=to_yson_type([b'component id 4']), key=b'queue4-4', storyPoints=4),
                Record(id='issue id 5', resolution=b'resolution id other', assignee=5, components=to_yson_type([b'component id 5']), key=b'queue5-5', storyPoints=5),
                Record(id='issue id 6', resolution=b'resolution id fixed', assignee=5, components=to_yson_type([b'component id 6']), key=b'queue6-6', storyPoints=6),  # Unused entry
            ]),
            'resolutions': local.StreamSource([
                Record(id=b'resolution id fixed',      key=b'fixed'),
                Record(id=b'resolution id incomplete', key=b'incomplete'),
                Record(id=b'resolution id other',      key=b'other'),
                Record(id=b'resolution id unused',     key=b'unusued'),  # Unused entry
            ]),
            'statuses': local.StreamSource([
                Record(id='status id closed', key=b'closed'),
                Record(id='status id open',   key=b'open'),
                Record(id='status id other',  key=b'other'),  # Unused entry
            ])
        },
        sinks={'result': local.ListSink(result)}
    )

    assert sorted([
        Record(staff_uid=3, component=b'component 3', ticket=b'queue3-3', story_points=3, date='2020-01-25'),
        Record(staff_uid=4, component=b'component 4', ticket=b'queue4-4', story_points=4, date='2020-01-25'),
    ]) == sorted(result)


def test_should_get_created_issues(job):
    result = []

    get_created_issues(
        {'2020-01-25'},
        mock_table(job, 'components'),
        mock_table(job, 'issues'),
    ).label('result')

    job.local_run(
        sources={
            'components': local.StreamSource([
                Record(id='component id 1', name=b'component 1'),
                Record(id='component id 2', name=b'component 2'),
            ]),

            'issues': local.StreamSource([
                Record(id=b'issue id 1', author=1, components=to_yson_type([b'component id 1']), key=b'queue1-1', created=timestamp('2020-01-25')),
                Record(id=b'issue id 2', author=2, components=to_yson_type([b'component id 2']), key=b'queue2-2', created=timestamp('2020-01-24'))
    ]),
        },
        sinks={'result': local.ListSink(result)}
    )

    assert sorted([
        Record(staff_uid=1, component=b'component 1', ticket=b'queue1-1', date='2020-01-25'),
    ]) == sorted(result)


def test_should_check_issues_for_absent_uid():
    with pytest.raises(RuntimeError, match='Staff UId is absent'):
        list(
            check_issues([
                Record(staff_uid=None, component=b'component 1', ticket=b'ticket-1', story_points=0.1),
            ])
        )


def test_should_check_issues_for_absent_component():
    with pytest.raises(RuntimeError, match='Component is absent'):
        list(
            check_issues([
                Record(staff_uid=1, component=None, ticket=b'ticket-1', story_points=0.1),
            ])
        )


def test_should_check_issues_for_unknown_component():
    with pytest.raises(RuntimeError, match="Unknown component"):
        list(
            check_issues([
                Record(staff_uid=1, component=_UNKNOWN_COMPONENT.encode(), ticket=b'ticket-1', story_points=0.1),
            ])
        )


def test_should_check_issues_for_absent_story_points():
    with pytest.raises(RuntimeError, match="Story points are absent"):
        list(
            check_issues([
                Record(staff_uid=1, component=b'component 1', ticket=b'ticket-1', story_points=None),
            ])
        )


def test_should_check_issues_for_several_error_types():
    with pytest.raises(RuntimeError) as e:
        list(
            check_issues([
                Record(staff_uid=1, component=None,           ticket=b'ticket-1', story_points=0.1),
                Record(staff_uid=1, component=b'component 1', ticket=b'ticket-1', story_points=None),
            ])
        )

    error = str(e.value)
    assert 'Staff UId is absent' not in error
    assert 'Component is absent' in error
    assert 'Unknown component' not in error
    assert 'Story points are absent' in error


def test_should_check_issues_without_changing_input():
    data = [
        Record(staff_uid=1, component=b'component 1', ticket=b'ticket-1', story_points=0.1),
        Record(staff_uid=2, component=b'component 2', ticket=b'ticket-2', story_points=0.2),
    ]

    assert list(check_issues(data)) == data
