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

import random

from hypothesis import given
from rest_framework.test import APITestCase
from unittest.mock import patch

from swh.model.hashutil import hash_to_hex

from swh.web.common.exc import NotFoundExc

from swh.web.common.utils import reverse, parse_timestamp
from swh.web.tests.strategies import (
    revision, unknown_revision, new_revision,
    unknown_origin_id, origin, origin_with_multiple_visits
)
from swh.web.tests.testcase import WebTestCase


class RevisionApiTestCase(WebTestCase, APITestCase):

    @given(revision())
    def test_api_revision(self, revision):

        url = reverse('api-revision', url_args={'sha1_git': revision})
        rv = self.client.get(url)

        expected_revision = self.revision_get(revision)

        self._enrich_revision(expected_revision)

        self.assertEqual(rv.status_code, 200)
        self.assertEqual(rv['Content-Type'], 'application/json')
        self.assertEqual(rv.data, expected_revision)

    @given(unknown_revision())
    def test_api_revision_not_found(self, unknown_revision):

        url = reverse('api-revision', url_args={'sha1_git': unknown_revision})
        rv = self.client.get(url)

        self.assertEqual(rv.status_code, 404)
        self.assertEqual(rv['Content-Type'], 'application/json')
        self.assertEqual(rv.data, {
            'exception': 'NotFoundExc',
            'reason': 'Revision with sha1_git %s not found.' %
            unknown_revision})

    @given(revision())
    def test_api_revision_raw_ok(self, revision):

        url = reverse('api-revision-raw-message',
                      url_args={'sha1_git': revision})
        rv = self.client.get(url)

        expected_message = self.revision_get(revision)['message']

        self.assertEqual(rv.status_code, 200)
        self.assertEqual(rv['Content-Type'], 'application/octet-stream')
        self.assertEqual(rv.content, expected_message.encode())

    @given(new_revision())
    def test_api_revision_raw_ok_no_msg(self, new_revision):

        del new_revision['message']
        self.storage.revision_add([new_revision])

        new_revision_id = hash_to_hex(new_revision['id'])

        url = reverse('api-revision-raw-message',
                      url_args={'sha1_git': new_revision_id})

        rv = self.client.get(url)

        self.assertEqual(rv.status_code, 404)
        self.assertEqual(rv['Content-Type'], 'application/json')
        self.assertEqual(rv.data, {
            'exception': 'NotFoundExc',
            'reason': 'No message for revision with sha1_git %s.' %
            new_revision_id})

    @given(unknown_revision())
    def test_api_revision_raw_ko_no_rev(self, unknown_revision):

        url = reverse('api-revision-raw-message',
                      url_args={'sha1_git': unknown_revision})
        rv = self.client.get(url)

        self.assertEqual(rv.status_code, 404)
        self.assertEqual(rv['Content-Type'], 'application/json')
        self.assertEqual(rv.data, {
            'exception': 'NotFoundExc',
            'reason': 'Revision with sha1_git %s not found.' %
            unknown_revision})

    @given(unknown_origin_id())
    def test_api_revision_with_origin_not_found(self, unknown_origin_id):

        url = reverse('api-revision-origin',
                      url_args={'origin_id': unknown_origin_id})
        rv = self.client.get(url)

        self.assertEqual(rv.status_code, 404)
        self.assertEqual(rv['Content-Type'], 'application/json')
        self.assertEqual(rv.data, {
            'exception': 'NotFoundExc',
            'reason': 'Origin with id %s not found!' %
            unknown_origin_id})

    @given(origin())
    def test_api_revision_with_origin(self, origin):

        url = reverse('api-revision-origin',
                      url_args={'origin_id': origin['id']})
        rv = self.client.get(url)

        snapshot = self.snapshot_get_latest(origin['id'])
        expected_revision = self.revision_get(
            snapshot['branches']['HEAD']['target'])

        self._enrich_revision(expected_revision)

        self.assertEqual(rv.status_code, 200)
        self.assertEqual(rv['Content-Type'], 'application/json')
        self.assertEqual(rv.data, expected_revision)

    @given(origin())
    def test_api_revision_with_origin_and_branch_name(self, origin):

        snapshot = self.snapshot_get_latest(origin['id'])

        branch_name = random.choice(
            list(b for b in snapshot['branches'].keys()
                 if snapshot['branches'][b]['target_type'] == 'revision'))

        url = reverse('api-revision-origin',
                      url_args={'origin_id': origin['id'],
                                'branch_name': branch_name})

        rv = self.client.get(url)

        expected_revision = self.revision_get(
            snapshot['branches'][branch_name]['target'])

        self._enrich_revision(expected_revision)

        self.assertEqual(rv.status_code, 200)
        self.assertEqual(rv['Content-Type'], 'application/json')
        self.assertEqual(rv.data, expected_revision)

    @given(origin_with_multiple_visits())
    def test_api_revision_with_origin_and_branch_name_and_ts(self, origin):

        visit = random.choice(self.origin_visit_get(origin['id']))

        snapshot = self.snapshot_get(visit['snapshot'])

        branch_name = random.choice(
            list(b for b in snapshot['branches'].keys()
                 if snapshot['branches'][b]['target_type'] == 'revision'))

        url = reverse('api-revision-origin',
                      url_args={'origin_id': origin['id'],
                                'branch_name': branch_name,
                                'ts': visit['date']})

        rv = self.client.get(url)

        expected_revision = self.revision_get(
            snapshot['branches'][branch_name]['target'])

        self._enrich_revision(expected_revision)

        self.assertEqual(rv.status_code, 200)
        self.assertEqual(rv['Content-Type'], 'application/json')
        self.assertEqual(rv.data, expected_revision)

    @given(origin_with_multiple_visits())
    def test_api_revision_with_origin_and_branch_name_and_ts_escapes(self,
                                                                     origin):
        visit = random.choice(self.origin_visit_get(origin['id']))

        snapshot = self.snapshot_get(visit['snapshot'])

        branch_name = random.choice(
            list(b for b in snapshot['branches'].keys()
                 if snapshot['branches'][b]['target_type'] == 'revision'))

        date = parse_timestamp(visit['date'])

        formatted_date = date.strftime('Today is %B %d, %Y at %X')

        url = reverse('api-revision-origin',
                      url_args={'origin_id': origin['id'],
                                'branch_name': branch_name,
                                'ts': formatted_date})

        rv = self.client.get(url)

        expected_revision = self.revision_get(
            snapshot['branches'][branch_name]['target'])

        self._enrich_revision(expected_revision)

        self.assertEqual(rv.status_code, 200)
        self.assertEqual(rv['Content-Type'], 'application/json')
        self.assertEqual(rv.data, expected_revision)

    @given(unknown_origin_id())
    def test_api_directory_through_revision_origin_ko(self,
                                                      unknown_origin_id):

        url = reverse('api-revision-origin-directory',
                      url_args={'origin_id': unknown_origin_id})
        rv = self.client.get(url)

        self.assertEqual(rv.status_code, 404)
        self.assertEqual(rv['Content-Type'], 'application/json')
        self.assertEqual(rv.data, {
            'exception': 'NotFoundExc',
            'reason': 'Origin with id %s not found!' %
            unknown_origin_id
        })

    @given(origin())
    def test_api_directory_through_revision_origin(self, origin):

        url = reverse('api-revision-origin-directory',
                      url_args={'origin_id': origin['id']})
        rv = self.client.get(url)

        snapshot = self.snapshot_get_latest(origin['id'])
        revision_id = snapshot['branches']['HEAD']['target']
        revision = self.revision_get(revision_id)
        directory = self.directory_ls(revision['directory'])

        for entry in directory:
            if entry['type'] == 'dir':
                entry['target_url'] = reverse(
                    'api-directory',
                    url_args={'sha1_git': entry['target']}
                )
                entry['dir_url'] = reverse(
                    'api-revision-origin-directory',
                    url_args={'origin_id': origin['id'],
                              'path': entry['name']})
            elif entry['type'] == 'file':
                entry['target_url'] = reverse(
                    'api-content',
                    url_args={'q': 'sha1_git:%s' % entry['target']}
                )
                entry['file_url'] = reverse(
                    'api-revision-origin-directory',
                    url_args={'origin_id': origin['id'],
                              'path': entry['name']})
            elif entry['type'] == 'rev':
                entry['target_url'] = reverse(
                    'api-revision',
                    url_args={'sha1_git': entry['target']}
                )
                entry['rev_url'] = reverse(
                    'api-revision-origin-directory',
                    url_args={'origin_id': origin['id'],
                              'path': entry['name']})

        expected_result = {
            'content': directory,
            'path': '.',
            'revision': revision_id,
            'type': 'dir'
        }

        self.assertEqual(rv.status_code, 200)
        self.assertEqual(rv['Content-Type'], 'application/json')
        self.assertEqual(rv.data, expected_result)

    @given(revision())
    def test_api_revision_log(self, revision):

        per_page = 10

        url = reverse('api-revision-log', url_args={'sha1_git': revision},
                      query_params={'per_page': per_page})

        rv = self.client.get(url)

        expected_log = self.revision_log(revision, limit=per_page+1)
        expected_log = list(map(self._enrich_revision, expected_log))

        has_next = len(expected_log) > per_page

        self.assertEqual(rv.status_code, 200)
        self.assertEqual(rv['Content-Type'], 'application/json')
        self.assertEqual(rv.data,
                         expected_log[:-1] if has_next else expected_log)

        if has_next:
            self.assertIn('Link', rv)
            next_log_url = reverse(
                'api-revision-log',
                url_args={'sha1_git': expected_log[-1]['id']},
                query_params={'per_page': per_page})
            self.assertIn(next_log_url, rv['Link'])

    @given(unknown_revision())
    def test_api_revision_log_not_found(self, unknown_revision):

        url = reverse('api-revision-log',
                      url_args={'sha1_git': unknown_revision})

        rv = self.client.get(url)

        self.assertEqual(rv.status_code, 404)
        self.assertEqual(rv['Content-Type'], 'application/json')
        self.assertEqual(rv.data, {
            'exception': 'NotFoundExc',
            'reason': 'Revision with sha1_git %s not found.' %
            unknown_revision})
        self.assertFalse(rv.has_header('Link'))

    @given(revision())
    def test_api_revision_log_context(self, revision):

        revisions = self.revision_log(revision, limit=4)

        prev_rev = revisions[0]['id']
        rev = revisions[-1]['id']

        per_page = 10

        url = reverse('api-revision-log',
                      url_args={'sha1_git': rev,
                                'prev_sha1s': prev_rev},
                      query_params={'per_page': per_page})

        rv = self.client.get(url)

        expected_log = self.revision_log(rev, limit=per_page)
        prev_revision = self.revision_get(prev_rev)
        expected_log.insert(0, prev_revision)
        expected_log = list(map(self._enrich_revision, expected_log))

        self.assertEqual(rv.status_code, 200)
        self.assertEqual(rv['Content-Type'], 'application/json')
        self.assertEqual(rv.data, expected_log)

    @given(origin())
    def test_api_revision_log_by(self, origin):

        per_page = 10

        url = reverse('api-revision-origin-log',
                      url_args={'origin_id': origin['id']},
                      query_params={'per_page': per_page})

        rv = self.client.get(url)

        snapshot = self.snapshot_get_latest(origin['id'])

        expected_log = self.revision_log(
            snapshot['branches']['HEAD']['target'], limit=per_page+1)

        expected_log = list(map(self._enrich_revision, expected_log))

        has_next = len(expected_log) > per_page

        self.assertEqual(rv.status_code, 200)
        self.assertEqual(rv['Content-Type'], 'application/json')
        self.assertEqual(rv.data,
                         expected_log[:-1] if has_next else expected_log)
        if has_next:
            self.assertIn('Link', rv)
            next_log_url = reverse(
                'api-revision-origin-log',
                url_args={'origin_id': origin['id'],
                          'branch_name': 'HEAD'},
                query_params={'per_page': per_page,
                              'sha1_git': expected_log[-1]['id']})
            self.assertIn(next_log_url, rv['Link'])

    @given(origin())
    def test_api_revision_log_by_ko(self, origin):

        invalid_branch_name = 'foobar'

        url = reverse('api-revision-origin-log',
                      url_args={'origin_id': origin['id'],
                                'branch_name': invalid_branch_name})

        rv = self.client.get(url)

        self.assertEqual(rv.status_code, 404)
        self.assertEqual(rv['Content-Type'], 'application/json')
        self.assertFalse(rv.has_header('Link'))
        self.assertEqual(
            rv.data,
            {'exception': 'NotFoundExc',
             'reason': 'Revision for origin %s and branch %s not found.' %
             (origin['id'], invalid_branch_name)})

    @patch('swh.web.api.views.revision._revision_directory_by')
    def test_api_revision_directory_ko_not_found(self, mock_rev_dir):
        # given
        mock_rev_dir.side_effect = NotFoundExc('Not found')

        # then
        rv = self.client.get('/api/1/revision/999/directory/some/path/to/dir/')

        self.assertEqual(rv.status_code, 404)
        self.assertEqual(rv['Content-Type'], 'application/json')
        self.assertEqual(rv.data, {
            'exception': 'NotFoundExc',
            'reason': 'Not found'})

        mock_rev_dir.assert_called_once_with(
            {'sha1_git': '999'},
            'some/path/to/dir',
            '/api/1/revision/999/directory/some/path/to/dir/',
            with_data=False)

    @patch('swh.web.api.views.revision._revision_directory_by')
    def test_api_revision_directory_ok_returns_dir_entries(self, mock_rev_dir):
        stub_dir = {
            'type': 'dir',
            'revision': '999',
            'content': [
                {
                    'sha1_git': '789',
                    'type': 'file',
                    'target': '101',
                    'target_url': '/api/1/content/sha1_git:101/',
                    'name': 'somefile',
                    'file_url': '/api/1/revision/999/directory/some/path/'
                    'somefile/'
                },
                {
                    'sha1_git': '123',
                    'type': 'dir',
                    'target': '456',
                    'target_url': '/api/1/directory/456/',
                    'name': 'to-subdir',
                    'dir_url': '/api/1/revision/999/directory/some/path/'
                    'to-subdir/',
                }]
        }

        # given
        mock_rev_dir.return_value = stub_dir

        # then
        rv = self.client.get('/api/1/revision/999/directory/some/path/')

        self.assertEqual(rv.status_code, 200)
        self.assertEqual(rv['Content-Type'], 'application/json')
        self.assertEqual(rv.data, stub_dir)

        mock_rev_dir.assert_called_once_with(
            {'sha1_git': '999'},
            'some/path',
            '/api/1/revision/999/directory/some/path/',
            with_data=False)

    @patch('swh.web.api.views.revision._revision_directory_by')
    def test_api_revision_directory_ok_returns_content(self, mock_rev_dir):
        stub_content = {
            'type': 'file',
            'revision': '999',
            'content': {
                'sha1_git': '789',
                'sha1': '101',
                'data_url': '/api/1/content/101/raw/',
            }
        }

        # given
        mock_rev_dir.return_value = stub_content

        # then
        url = '/api/1/revision/666/directory/some/other/path/'
        rv = self.client.get(url)

        self.assertEqual(rv.status_code, 200)
        self.assertEqual(rv['Content-Type'], 'application/json')
        self.assertEqual(rv.data, stub_content)

        mock_rev_dir.assert_called_once_with(
            {'sha1_git': '666'}, 'some/other/path', url, with_data=False)

    def _enrich_revision(self, revision):
        author_url = reverse(
            'api-person',
            url_args={'person_id': revision['author']['id']})

        committer_url = reverse(
            'api-person',
            url_args={'person_id': revision['committer']['id']})

        directory_url = reverse(
            'api-directory',
            url_args={'sha1_git': revision['directory']})

        history_url = reverse('api-revision-log',
                              url_args={'sha1_git': revision['id']})

        parents_id_url = []
        for p in revision['parents']:
            parents_id_url.append({
                'id': p,
                'url': reverse('api-revision', url_args={'sha1_git': p})
            })

        revision_url = reverse('api-revision',
                               url_args={'sha1_git': revision['id']})

        revision['author_url'] = author_url
        revision['committer_url'] = committer_url
        revision['directory_url'] = directory_url
        revision['history_url'] = history_url
        revision['url'] = revision_url
        revision['parents'] = parents_id_url

        return revision

    @given(revision())
    def test_api_revision_uppercase(self, revision):
        url = reverse('api-revision-uppercase-checksum',
                      url_args={'sha1_git': revision.upper()})

        resp = self.client.get(url)
        self.assertEqual(resp.status_code, 302)

        redirect_url = reverse('api-revision',
                               url_args={'sha1_git': revision})

        self.assertEqual(resp['location'], redirect_url)
