# Copyright 2016 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.


import os
import time
import yaml

from tests.base import ZuulTestCase, simple_layout


class TestGitDriver(ZuulTestCase):
    config_file = 'zuul-git-driver.conf'
    tenant_config_file = 'config/git-driver/main.yaml'

    def setUp(self):
        super(TestGitDriver, self).setUp()
        self.git_connection = self.scheds.first.sched.connections\
            .getSource('git').connection

    def setup_config(self, config_file: str):
        config = super(TestGitDriver, self).setup_config(config_file)
        config.set('connection git', 'baseurl', self.upstream_root)
        return config

    def test_basic(self):
        tenant = self.scheds.first.sched.abide.tenants.get('tenant-one')
        # Check that we have the git source for common-config and the
        # gerrit source for the project.
        self.assertEqual('git', list(tenant.config_projects)[0].source.name)
        self.assertEqual('common-config', list(tenant.config_projects)[0].name)
        self.assertEqual('gerrit',
                         list(tenant.untrusted_projects)[0].source.name)
        self.assertEqual('org/project',
                         list(tenant.untrusted_projects)[0].name)

        # The configuration for this test is accessed via the git
        # driver (in common-config), rather than the gerrit driver, so
        # if the job runs, it worked.
        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
        self.waitUntilSettled()
        self.assertEqual(len(self.history), 1)
        self.assertEqual(A.reported, 1)

    def test_config_refreshed(self):
        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
        self.waitUntilSettled()
        self.assertEqual(len(self.history), 1)
        self.assertEqual(A.reported, 1)
        self.assertEqual(self.history[0].name, 'project-test1')

        # Update zuul.yaml to force a tenant reconfiguration
        path = os.path.join(self.upstream_root, 'common-config', 'zuul.yaml')
        with open(path, 'r') as f:
            config = yaml.safe_load(f)
        change = {
            'name': 'org/project',
            'check': {
                'jobs': [
                    'project-test2'
                ]
            }
        }
        config[4]['project'] = change
        files = {'zuul.yaml': yaml.dump(config)}
        self.addCommitToRepo(
            'common-config', 'Change zuul.yaml configuration', files)

        # Wait for the tenant reconfiguration to happen
        count = self.waitForEvent()
        self.waitUntilSettled()

        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
        self.waitUntilSettled()
        self.assertEqual(len(self.history), 2)
        self.assertEqual(A.reported, 1)
        # We make sure the new job has run
        self.assertEqual(self.history[1].name, 'project-test2')

        # Let's stop the git Watcher to let us merge some changes commits
        # We want to verify that config changes are detected for commits
        # on the range oldrev..newrev
        self.scheds.first.sched.connections.getSource('git').connection\
            .watcher_thread._pause = True
        # Add a config change
        change = {
            'name': 'org/project',
            'check': {
                'jobs': [
                    'project-test1'
                ]
            }
        }
        config[4]['project'] = change
        files = {'zuul.yaml': yaml.dump(config)}
        self.addCommitToRepo(
            'common-config', 'Change zuul.yaml configuration', files)
        # Add two other changes
        self.addCommitToRepo(
            'common-config', 'Adding f1',
            {'f1': "Content"})
        self.addCommitToRepo(
            'common-config', 'Adding f2',
            {'f2': "Content"})
        # Restart the git watcher
        self.scheds.first.sched.connections.getSource('git').connection\
            .watcher_thread._pause = False

        # Wait for the tenant reconfiguration to happen
        self.waitForEvent(count)
        self.waitUntilSettled()

        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
        self.waitUntilSettled()
        self.assertEqual(len(self.history), 3)
        self.assertEqual(A.reported, 1)
        # We make sure the new job has run
        self.assertEqual(self.history[2].name, 'project-test1')

    def ensure_watcher_has_context(self):
        # Make sure watcher have read initial refs shas
        delay = 0.1
        max_delay = 1
        while not self.git_connection.watcher_thread.projects_refs:
            time.sleep(delay)
            max_delay -= delay
            if max_delay <= 0:
                raise Exception("Timeout waiting for initial read")
        return self.git_connection.watcher_thread._event_count

    def waitForEvent(self, initial_count=0):
        delay = 0.1
        max_delay = 5
        while self.git_connection.watcher_thread._event_count <= initial_count:
            time.sleep(delay)
            max_delay -= delay
            if max_delay <= 0:
                raise Exception("Timeout waiting for event")
        return self.git_connection.watcher_thread._event_count

    @simple_layout('layouts/basic-git.yaml', driver='git')
    def test_ref_updated_event(self):
        count = self.ensure_watcher_has_context()
        # Add a commit to trigger a ref-updated event
        self.addCommitToRepo(
            'org/project', 'A change for ref-updated', {'f1': 'Content'})
        # Wait for the git watcher to detect the ref-update event
        self.waitForEvent(count)
        self.waitUntilSettled()
        self.assertEqual(len(self.history), 1)
        self.assertEqual('SUCCESS',
                         self.getJobFromHistory('post-job').result)

    @simple_layout('layouts/basic-git.yaml', driver='git')
    def test_ref_created(self):
        count = self.ensure_watcher_has_context()
        # Tag HEAD to trigger a ref-updated event
        self.addTagToRepo(
            'org/project', 'atag', 'HEAD')
        # Wait for the git watcher to detect the ref-update event
        self.waitForEvent(count)
        self.waitUntilSettled()
        self.assertEqual(len(self.history), 1)
        self.assertEqual('SUCCESS',
                         self.getJobFromHistory('tag-job').result)

    @simple_layout('layouts/basic-git.yaml', driver='git')
    def test_ref_deleted(self):
        count = self.ensure_watcher_has_context()
        # Delete default tag init to trigger a ref-updated event
        self.delTagFromRepo(
            'org/project', 'init')
        # Wait for the git watcher to detect the ref-update event
        self.waitForEvent(count)
        self.waitUntilSettled()
        # Make sure no job as run as ignore-delete is True by default
        self.assertEqual(len(self.history), 0)

    @simple_layout('layouts/basic-git.yaml', driver='git')
    def test_get_project_branch_sha(self):
        # Exercise this method since it's only called from timer
        # triggers
        source = self.scheds.first.sched.connections.getSource('git')
        project = source.getProject('org/project')
        self.assertIsNotNone(source.getProjectBranchSha(project, 'master'))
