import datetime
from firebase_admin import messaging
from firebase_admin.exceptions import FirebaseError, InvalidArgumentError, InternalError, UnavailableError
from firebase_admin.messaging import QuotaExceededError, SenderIdMismatchError, ThirdPartyAuthError, UnregisteredError
import json
from mock import patch, Mock, ANY
import unittest2

from google.appengine.api.taskqueue import taskqueue
from google.appengine.ext import deferred
from google.appengine.ext import ndb
from google.appengine.ext import testbed

from consts.award_type import AwardType
from consts.client_type import ClientType
from consts.event_type import EventType
from consts.model_type import ModelType
from consts.notification_type import NotificationType
import helpers.tbans_helper
from helpers.event.event_test_creator import EventTestCreator
from helpers.match.match_test_creator import MatchTestCreator
from helpers.tbans_helper import TBANSHelper, _firebase_app
from models.account import Account
from models.award import Award
from models.event import Event
from models.event_details import EventDetails
from models.match import Match
from models.team import Team
from models.mobile_client import MobileClient
from models.subscription import Subscription
from models.notifications.alliance_selection import AllianceSelectionNotification
from models.notifications.awards import AwardsNotification
from models.notifications.broadcast import BroadcastNotification
from models.notifications.event_level import EventLevelNotification
from models.notifications.event_schedule import EventScheduleNotification
from models.notifications.match_score import MatchScoreNotification
from models.notifications.match_upcoming import MatchUpcomingNotification
from models.notifications.match_video import MatchVideoNotification
from models.notifications.requests.fcm_request import FCMRequest
from models.notifications.requests.webhook_request import WebhookRequest

from tests.mocks.notifications.mock_notification import MockNotification


def fcm_messaging_ids(user_id):
    clients = MobileClient.query(
        MobileClient.client_type.IN(ClientType.FCM_CLIENTS),
        ancestor=ndb.Key(Account, user_id)
    ).fetch(projection=[MobileClient.messaging_id])
    return [c.messaging_id for c in clients]


class TestTBANSHelper(unittest2.TestCase):

    def setUp(self):
        self.testbed = testbed.Testbed()
        self.testbed.activate()
        self.testbed.init_datastore_v3_stub()
        self.testbed.init_memcache_stub()
        self.testbed.init_app_identity_stub()
        self.testbed.init_taskqueue_stub(root_path='.')
        self.taskqueue_stub = self.testbed.get_stub(testbed.TASKQUEUE_SERVICE_NAME)
        ndb.get_context().clear_cache()  # Prevent data from leaking between tests
        self.event = EventTestCreator.createFutureEvent()
        self.team = Team(
            id='frc7332',
            team_number=7332
        )
        self.team.put()
        self.match = Match(
            id='2020miket_qm1',
            event=self.event.key,
            comp_level='qm',
            set_number=1,
            match_number=1,
            team_key_names=['frc7332'],
            alliances_json=json.dumps({
                'red': {
                    'teams': ['frc1', 'frc2', 'frc7332'],
                    'score': -1,
                },
                'blue': {
                    'teams': ['frc4', 'frc5', 'frc6'],
                    'score': -1,
                }
            }),
            year=2020
        )

    def tearDown(self):
        self.testbed.deactivate()

    def test_firebase_app(self):
        # Make sure we can get an original Firebase app
        app_one = _firebase_app()
        self.assertIsNotNone(app_one)
        self.assertEqual(app_one.name, 'tbans')
        # Make sure duplicate calls don't crash
        app_two = _firebase_app()
        self.assertIsNotNone(app_two)
        self.assertEqual(app_two.name, 'tbans')
        # Should be the same object
        self.assertEqual(app_one, app_two)

    def test_alliance_selection_no_users(self):
        # Test send not called with no subscribed users
        with patch.object(TBANSHelper, '_send') as mock_send:
            TBANSHelper.alliance_selection(self.event)
            mock_send.assert_not_called()

    def test_alliance_selection_user_id(self):
        # Test send called with user id
        with patch.object(TBANSHelper, '_send') as mock_send:
            TBANSHelper.alliance_selection(self.event, 'user_id')
            mock_send.assert_called_once()
            user_id = mock_send.call_args[0][0]
            self.assertEqual(user_id, ['user_id'])

    def test_alliance_selection(self):
        # Insert a Subscription for this Event and these Teams so we call to send
        Subscription(
            parent=ndb.Key(Account, 'user_id_1'),
            user_id='user_id_1',
            model_key='frc1',
            model_type=ModelType.TEAM,
            notification_types=[NotificationType.ALLIANCE_SELECTION]
        ).put()
        Subscription(
            parent=ndb.Key(Account, 'user_id_2'),
            user_id='user_id_2',
            model_key='frc7332',
            model_type=ModelType.TEAM,
            notification_types=[NotificationType.ALLIANCE_SELECTION]
        ).put()
        Subscription(
            parent=ndb.Key(Account, 'user_id_3'),
            user_id='user_id_3',
            model_key=self.event.key_name,
            model_type=ModelType.EVENT,
            notification_types=[NotificationType.ALLIANCE_SELECTION]
        ).put()

        # Insert EventDetails for the event with alliance selection information
        EventDetails(
            id=self.event.key_name,
            alliance_selections=[
                {"declines": [], "picks": ["frc7332"]}
            ]
        ).put()

        with patch.object(TBANSHelper, '_send') as mock_send:
            TBANSHelper.alliance_selection(self.event)
            # Two calls total - First to the Event, second to frc7332, no call for frc1
            mock_send.assert_called()
            self.assertEqual(len(mock_send.call_args_list), 2)
            self.assertEqual([x[0] for x in [call[0][0] for call in mock_send.call_args_list]], ['user_id_3', 'user_id_2'])
            notifications = [call[0][1] for call in mock_send.call_args_list]
            for notification in notifications:
                self.assertTrue(isinstance(notification, AllianceSelectionNotification))
            # Check Event notification
            event_notification = notifications[0]
            self.assertEqual(event_notification.event, self.event)
            self.assertIsNone(event_notification.team)
            # Check frc7332 notification
            team_notification = notifications[1]
            self.assertEqual(team_notification.event, self.event)
            self.assertEqual(team_notification.team, self.team)

    def test_awards_no_users(self):
        # Test send not called with no subscribed users
        with patch.object(TBANSHelper, '_send') as mock_send:
            TBANSHelper.awards(self.event)
            mock_send.assert_not_called()

    def test_awards_user_id(self):
        # Test send called with user id
        with patch.object(TBANSHelper, '_send') as mock_send:
            TBANSHelper.awards(self.event, 'user_id')
            mock_send.assert_called_once()
            user_id = mock_send.call_args[0][0]
            self.assertEqual(user_id, ['user_id'])

    def test_awards(self):
        # Insert some Awards for some Teams
        award = Award(
            id=Award.render_key_name(self.event.key_name, AwardType.INDUSTRIAL_DESIGN),
            name_str='Industrial Design Award sponsored by General Motors',
            award_type_enum=AwardType.INDUSTRIAL_DESIGN,
            event=self.event.key,
            event_type_enum=EventType.REGIONAL,
            team_list=[ndb.Key(Team, 'frc7332')],
            year=2020
        )
        award.put()
        winner_award = Award(
            id=Award.render_key_name(self.event.key_name, AwardType.WINNER),
            name_str='Regional Event Winner',
            award_type_enum=AwardType.WINNER,
            event=self.event.key,
            event_type_enum=EventType.REGIONAL,
            team_list=[ndb.Key(Team, 'frc7332'), ndb.Key(Team, 'frc1')],
            year=2020
        )
        winner_award.put()
        frc_1 = Team(
            id='frc1',
            team_number=1
        )
        frc_1.put()

        # Insert a Subscription for this Event and these Teams so we call to send
        Subscription(
            parent=ndb.Key(Account, 'user_id_1'),
            user_id='user_id_1',
            model_key='frc1',
            model_type=ModelType.TEAM,
            notification_types=[NotificationType.AWARDS]
        ).put()
        Subscription(
            parent=ndb.Key(Account, 'user_id_2'),
            user_id='user_id_2',
            model_key='frc7332',
            model_type=ModelType.TEAM,
            notification_types=[NotificationType.AWARDS]
        ).put()
        Subscription(
            parent=ndb.Key(Account, 'user_id_3'),
            user_id='user_id_3',
            model_key=self.event.key_name,
            model_type=ModelType.EVENT,
            notification_types=[NotificationType.AWARDS]
        ).put()

        with patch.object(TBANSHelper, '_send') as mock_send:
            TBANSHelper.awards(self.event)
            # Three calls total - First to the Event, second to frc7332 (two awards), third to frc1 (one award)
            mock_send.assert_called()
            self.assertEqual(len(mock_send.call_args_list), 3)
            self.assertEqual([x[0] for x in [call[0][0] for call in mock_send.call_args_list]], ['user_id_3', 'user_id_1', 'user_id_2'])
            notifications = [call[0][1] for call in mock_send.call_args_list]
            for notification in notifications:
                self.assertTrue(isinstance(notification, AwardsNotification))
            # Check Event notification
            event_notification = notifications[0]
            self.assertEqual(event_notification.event, self.event)
            self.assertIsNone(event_notification.team)
            self.assertEqual(event_notification.team_awards, [])
            # Check frc1 notification
            event_notification = notifications[1]
            self.assertEqual(event_notification.event, self.event)
            self.assertEqual(event_notification.team, frc_1)
            self.assertEqual(len(event_notification.team_awards), 1)
            # Check frc7332 notification
            event_notification = notifications[2]
            self.assertEqual(event_notification.event, self.event)
            self.assertEqual(event_notification.team, self.team)
            self.assertEqual(len(event_notification.team_awards), 2)

    def test_broadcast_none(self):
        from notifications.base_notification import BaseNotification
        with patch.object(BaseNotification, 'send') as mock_send:
            TBANSHelper.broadcast([], 'Broadcast', 'Test broadcast')
            # Make sure we didn't send to Android
            mock_send.assert_not_called()

        # Make sure we didn't send to FCM or webhooks
        tasks = self.taskqueue_stub.GetTasks('push-notifications')
        self.assertEqual(len(tasks), 0)

    def test_broadcast_fcm_empty(self):
        from notifications.base_notification import BaseNotification

        for client_type in ClientType.FCM_CLIENTS:
            with patch.object(BaseNotification, 'send') as mock_send:
                TBANSHelper.broadcast([client_type], 'Broadcast', 'Test broadcast')
                # Make sure we didn't send to Android
                mock_send.assert_not_called()

            # Make sure we didn't send to FCM or webhooks
            tasks = self.taskqueue_stub.GetTasks('push-notifications')
            self.assertEqual(len(tasks), 0)

    def test_broadcast_fcm(self):
        for client_type in ClientType.FCM_CLIENTS:
            client = MobileClient(
                parent=ndb.Key(Account, 'user_id'),
                user_id='user_id',
                messaging_id='token',
                client_type=client_type,
                device_uuid='uuid',
                display_name='Phone')
            client_key = client.put()

            from notifications.base_notification import BaseNotification
            with patch.object(BaseNotification, 'send') as mock_send:
                TBANSHelper.broadcast([client_type], 'Broadcast', 'Test broadcast')
                # Make sure we didn't send to Android
                mock_send.assert_not_called()

            # Make sure we'll send to FCM clients
            tasks = self.taskqueue_stub.get_filtered_tasks(queue_names='push-notifications')
            self.assertEqual(len(tasks), 1)

            # Make sure our taskqueue tasks execute what we expect
            with patch.object(TBANSHelper, '_send_fcm') as mock_send_fcm:
                deferred.run(tasks[0].payload)
                mock_send_fcm.assert_called_once_with([client], ANY)
                # Make sure the notification is a BroadcastNotification
                notification = mock_send_fcm.call_args[0][1]
                self.assertTrue(isinstance(notification, BroadcastNotification))

            self.taskqueue_stub.FlushQueue('push-notifications')

            client_key.delete()

    def test_broadcast_webhook_empty(self):
        from notifications.base_notification import BaseNotification

        with patch.object(BaseNotification, 'send') as mock_send:
            TBANSHelper.broadcast([ClientType.WEBHOOK], 'Broadcast', 'Test broadcast')
            # Make sure we didn't send to Android
            mock_send.assert_not_called()

        # Make sure we didn't send to FCM or webhooks
        tasks = self.taskqueue_stub.GetTasks('push-notifications')
        self.assertEqual(len(tasks), 0)

    def test_broadcast_webhook(self):
        from notifications.base_notification import BaseNotification

        client = MobileClient(
            parent=ndb.Key(Account, 'user_id'),
            user_id='user_id',
            messaging_id='token',
            client_type=ClientType.WEBHOOK,
            device_uuid='uuid',
            display_name='Phone')
        client_key = client.put()

        with patch.object(BaseNotification, 'send') as mock_send:
            TBANSHelper.broadcast([ClientType.WEBHOOK], 'Broadcast', 'Test broadcast')
            # Make sure we didn't send to Android
            mock_send.assert_not_called()

        # Make sure we'll send to FCM clients
        tasks = self.taskqueue_stub.get_filtered_tasks(queue_names='push-notifications')
        self.assertEqual(len(tasks), 1)

        # Make sure our taskqueue tasks execute what we expect
        with patch.object(TBANSHelper, '_send_webhook') as mock_send_webhook:
            deferred.run(tasks[0].payload)
            mock_send_webhook.assert_called_once_with([client], ANY)
            # Make sure the notification is a BroadcastNotification
            notification = mock_send_webhook.call_args[0][1]
            self.assertTrue(isinstance(notification, BroadcastNotification))

    def test_broadcast_android(self):
        client_type = ClientType.OS_ANDROID
        messaging_id = 'token'

        client = MobileClient(
            parent=ndb.Key(Account, 'user_id'),
            user_id='user_id',
            messaging_id=messaging_id,
            client_type=client_type,
            device_uuid='uuid',
            display_name='Phone')
        client.put()

        from notifications.broadcast import BroadcastNotification
        with patch.object(BroadcastNotification, 'send') as mock_send:
            TBANSHelper.broadcast([client_type], 'Broadcast', 'Test broadcast')
            mock_send.assert_called_once_with({client_type: [messaging_id]})

        # Make sure we didn't send to FCM or webhooks
        tasks = self.taskqueue_stub.GetTasks('push-notifications')
        self.assertEqual(len(tasks), 0)

    def test_event_level_no_users(self):
        # Test send not called with no subscribed users
        with patch.object(TBANSHelper, '_send') as mock_send:
            TBANSHelper.event_level(self.match)
            mock_send.assert_not_called()

    def test_event_level_user_id(self):
        # Test send called with user id
        with patch.object(TBANSHelper, '_send') as mock_send:
            TBANSHelper.event_level(self.match, 'user_id')
            mock_send.assert_called()
            self.assertEqual(len(mock_send.call_args_list), 1)
            for call in mock_send.call_args_list:
                self.assertEqual(call[0][0], ['user_id'])

    def test_event_level(self):
        # Insert a Subscription for this Event
        Subscription(
            parent=ndb.Key(Account, 'user_id_1'),
            user_id='user_id_1',
            model_key=self.event.key_name,
            model_type=ModelType.EVENT,
            notification_types=[NotificationType.LEVEL_STARTING]
        ).put()

        with patch.object(TBANSHelper, '_send') as mock_send:
            TBANSHelper.event_level(self.match)
            mock_send.assert_called()
            self.assertEqual(len(mock_send.call_args_list), 1)
            user_ids = mock_send.call_args[0][0]
            self.assertEqual(user_ids, ['user_id_1'])
            notification = mock_send.call_args[0][1]
            self.assertTrue(isinstance(notification, EventLevelNotification))
            self.assertEqual(notification.match, self.match)
            self.assertEqual(notification.event, self.event)

    def test_event_schedule_no_users(self):