#!/usr/bin/env vpython3
# Copyright 2021 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import json
import os
import sys
import unittest

from unittest import mock

_BUILD_UTIL_PATH = os.path.abspath(
    os.path.join(os.path.dirname(__file__), '..', '..')
)
if _BUILD_UTIL_PATH not in sys.path:
    sys.path.insert(0, _BUILD_UTIL_PATH)

from lib.results import result_sink
from lib.results import result_types

_FAKE_CONTEXT = {
    'address': 'some-ip-address',
    'auth_token': 'some-auth-token',
}


class InitClientTest(unittest.TestCase):
    @mock.patch.dict(os.environ, {}, clear=True)
    def testEmptyClient(self):
        # No LUCI_CONTEXT env var should prevent a client from being created.
        client = result_sink.TryInitClient()
        self.assertIsNone(client)

    @mock.patch.dict(os.environ, {'LUCI_CONTEXT': 'some-file.json'})
    def testBasicClient(self):
        luci_context_json = {
            'result_sink': _FAKE_CONTEXT,
        }
        with mock.patch(
            'builtins.open',
            mock.mock_open(read_data=json.dumps(luci_context_json)),
        ):
            client = result_sink.TryInitClient()
        self.assertEqual(
            client.test_results_url,
            'http://some-ip-address/prpc/luci.resultsink.v1.Sink/ReportTestResults',
        )
        self.assertEqual(
            client.session.headers['Authorization'],
            'ResultSink some-auth-token',
        )

    @mock.patch('requests.Session')
    def testReuseSession(self, mock_session):
        client = result_sink.ResultSinkClient(_FAKE_CONTEXT)
        client.Post('some-test', result_types.PASS, 0, 'some-test-log', None)
        client.Post('some-test', result_types.PASS, 0, 'some-test-log', None)
        self.assertEqual(mock_session.call_count, 1)
        self.assertEqual(client.session.post.call_count, 2)

    @mock.patch('requests.Session.close')
    def testCloseClient(self, mock_close):
        client = result_sink.ResultSinkClient(_FAKE_CONTEXT)
        client.close()
        mock_close.assert_called_once()

    @mock.patch('requests.Session.close')
    def testClientAsContextManager(self, mock_close):
        with result_sink.ResultSinkClient(_FAKE_CONTEXT) as client:
            mock_close.assert_not_called()
        mock_close.assert_called_once()


class ClientTest(unittest.TestCase):
    def setUp(self):
        self.client = result_sink.ResultSinkClient(_FAKE_CONTEXT)

    @mock.patch('requests.Session.post')
    def testPostPassingTest(self, mock_post):
        self.client.Post(
            'some-test', result_types.PASS, 0, 'some-test-log', None
        )
        self.assertEqual(
            mock_post.call_args[1]['url'],
            'http://some-ip-address/prpc/luci.resultsink.v1.Sink/ReportTestResults',
        )
        data = json.loads(mock_post.call_args[1]['data'])
        self.assertEqual(data['testResults'][0]['testId'], 'some-test')
        self.assertEqual(data['testResults'][0]['status'], 'PASS')

    @mock.patch('requests.Session.post')
    def testPostFailingTest(self, mock_post):
        self.client.Post(
            'some-test',
            result_types.FAIL,
            0,
            'some-test-log',
            None,
            failure_reason='omg test failure',
        )
        data = json.loads(mock_post.call_args[1]['data'])
        self.assertEqual(data['testResults'][0]['status'], 'FAIL')
        self.assertEqual(
            data['testResults'][0]['testMetadata']['name'], 'some-test'
        )
        self.assertEqual(
            data['testResults'][0]['failureReason']['primaryErrorMessage'],
            'omg test failure',
        )

    @mock.patch('requests.Session.post')
    def testPostWithTestLogAndHTMLSummary(self, mock_post):
        # This is under max length, but will be over when test log
        # artifact is included.
        test_artifact = (
            '<text-artifact artifact-id="%s" />'
            % 'b'
            * (result_sink.HTML_SUMMARY_MAX - 35)
        )
        self.client.Post(
            'some-test',
            result_types.PASS,
            0,
            'some-test-log',
            '//some/test.cc',
            html_artifact=test_artifact,
        )
        data = json.loads(mock_post.call_args[1]['data'])
        self.assertIsNotNone(data['testResults'][0]['summaryHtml'])
        self.assertTrue(
            len(data['testResults'][0]['summaryHtml'])
            < result_sink.HTML_SUMMARY_MAX
        )
        self.assertTrue(
            result_sink._HTML_SUMMARY_ARTIFACT
            in data['testResults'][0]['summaryHtml']
        )
        self.assertTrue(
            result_sink._TEST_LOG_ARTIFACT
            in data['testResults'][0]['summaryHtml']
        )

    @mock.patch('requests.Session.post')
    def testPostWithTooLongSummary(self, mock_post):
        # This will be over max length.
        test_artifact = (
            '<text-artifact artifact-id="%s" />'
            % 'b'
            * result_sink.HTML_SUMMARY_MAX
        )
        self.client.Post(
            'some-test',
            result_types.PASS,
            0,
            'some-test-log',
            '//some/test.cc',
            html_artifact=test_artifact,
        )
        data = json.loads(mock_post.call_args[1]['data'])
        self.assertIsNotNone(data['testResults'][0]['summaryHtml'])
        self.assertTrue(
            len(data['testResults'][0]['summaryHtml'])
            < result_sink.HTML_SUMMARY_MAX
        )
        self.assertTrue(
            result_sink._HTML_SUMMARY_ARTIFACT
            in data['testResults'][0]['summaryHtml']
        )

    @mock.patch('requests.Session.post')
    def testPostWithTestFile(self, mock_post):
        self.client.Post(
            'some-test', result_types.PASS, 0, 'some-test-log', '//some/test.cc'
        )
        data = json.loads(mock_post.call_args[1]['data'])
        self.assertEqual(
            data['testResults'][0]['testMetadata']['location']['file_name'],
            '//some/test.cc',
        )
        self.assertEqual(
            data['testResults'][0]['testMetadata']['name'], 'some-test'
        )
        self.assertIsNotNone(data['testResults'][0]['summaryHtml'])

    @mock.patch('requests.Session.post')
    def testPostWithVariant(self, mock_post):
        self.client.Post(
            'some-test',
            result_types.PASS,
            0,
            'some-test-log',
            None,
            variant={'key1': 'value1', 'key2': 'value2'},
        )
        data = json.loads(mock_post.call_args[1]['data'])
        self.assertEqual(
            data['testResults'][0]['variant'],
            {'def': {'key1': 'value1', 'key2': 'value2'}},
        )

    @mock.patch('requests.Session.post')
    def testPostWithTags(self, mock_post):
        self.client.Post(
            'some-test',
            result_types.PASS,
            0,
            'some-test-log',
            None,
            tags=[('key1', 'value1'), ('key2', 'value2')],
        )
        data = json.loads(mock_post.call_args[1]['data'])
        self.assertIn(
            {'key': 'key1', 'value': 'value1'}, data['testResults'][0]['tags']
        )
        self.assertIn(
            {'key': 'key2', 'value': 'value2'}, data['testResults'][0]['tags']
        )


if __name__ == '__main__':
    unittest.main()
