##############################################################################
#
# Copyright (c) 2018 Nexedi SA and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################

import glob
import hashlib
import json
import os
import re
import requests
import subprocess
import xml.etree.ElementTree as ET
from slapos.recipe.librecipe import generateHashFromFiles
from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass

setUpModule, SlapOSInstanceTestCase = makeModuleSetUpAndTestCaseClass(
    os.path.abspath(
        os.path.join(os.path.dirname(__file__), '..', 'software.cfg')))


class ServicesTestCase(SlapOSInstanceTestCase):

  def test_hashes(self):
    hash_files = [
      'software_release/buildout.cfg',
    ]
    expected_process_names = [
      'monitor-httpd-{hash}-on-watch',
      'crond-{hash}-on-watch',
    ]

    with self.slap.instance_supervisor_rpc as supervisor:
      process_names = [process['name']
                       for process in supervisor.getAllProcessInfo()]

    hash_files = [os.path.join(self.computer_partition_root_path, path)
                  for path in hash_files]

    for name in expected_process_names:
      h = generateHashFromFiles(hash_files)
      expected_process_name = name.format(hash=h)

      self.assertIn(expected_process_name, process_names)


class MonitorTestMixin:
  monitor_setup_url_key = 'monitor-setup-url'

  def test_monitor_setup(self):
    connection_parameter_dict_serialised = self\
      .computer_partition.getConnectionParameterDict()
    connection_parameter_dict = json.loads(
      connection_parameter_dict_serialised['_'])
    self.assertTrue(
      self.monitor_setup_url_key in connection_parameter_dict,
      '%s not in %s' % (self.monitor_setup_url_key, connection_parameter_dict))
    monitor_setup_url_value = connection_parameter_dict[
      self.monitor_setup_url_key]
    monitor_url_match = re.match(r'.*url=(.*)', monitor_setup_url_value)
    self.assertNotEqual(
      None, monitor_url_match, '%s not parsable' % (monitor_setup_url_value,))
    self.assertEqual(1, len(monitor_url_match.groups()))
    monitor_url = monitor_url_match.groups()[0]
    monitor_url_split = monitor_url.split('&')
    self.assertEqual(
      3, len(monitor_url_split), '%s not splitabble' % (monitor_url,))
    self.monitor_url = monitor_url_split[0]
    monitor_username = monitor_url_split[1].split('=')
    self.assertEqual(
      2, len(monitor_username), '%s not splittable' % (monitor_username))
    monitor_password = monitor_url_split[2].split('=')
    self.assertEqual(
      2, len(monitor_password), '%s not splittable' % (monitor_password))
    self.monitor_username = monitor_username[1]
    self.monitor_password = monitor_password[1]

    opml_text = requests.get(self.monitor_url, verify=False).text
    opml = ET.fromstring(opml_text)

    body = opml[1]
    self.assertEqual('body', body.tag)

    outline_list = body[0].findall('outline')

    self.assertEqual(
      self.monitor_configuration_list,
      [q.attrib for q in outline_list]
    )

    expected_status_code_list = []
    got_status_code_list = []
    for monitor_configuration in self.monitor_configuration_list:
      status_code = requests.get(
          monitor_configuration['url'],
          verify=False,
          auth=(self.monitor_username, self.monitor_password)
        ).status_code
      expected_status_code_list.append(
        {
          'url': monitor_configuration['url'],
          'status_code': 200
        }
      )
      got_status_code_list.append(
        {
          'url': monitor_configuration['url'],
          'status_code': status_code
        }
      )
    self.assertEqual(
      expected_status_code_list,
      got_status_code_list
    )


class EdgeMixin(object):
  __partition_reference__ = 'edge'
  instance_max_retry = 20
  expected_connection_parameter_dict = {}

  def setUp(self):
    self.updateSurykatkaDict()

  def assertSurykatkaIni(self):
    expected_init_path_list = []
    for instance_reference in self.surykatka_dict:
      expected_init_path_list.extend(
        [q['ini-file']
         for q in self.surykatka_dict[instance_reference].values()])
    self.assertEqual(
      set(
        glob.glob(
          os.path.join(
            self.slap.instance_directory, '*', 'etc', 'surykatka*.ini'
          )
        )
      ),
      set(expected_init_path_list)
    )
    for instance_reference in self.surykatka_dict:
      for info_dict in self.surykatka_dict[instance_reference].values():
        with open(info_dict['ini-file']) as fh:
          self.assertEqual(
            info_dict['expected_ini'].strip() % info_dict,
            fh.read().strip()
          )

  def assertPromiseContent(self, instance_reference, name, content):
    with open(
      os.path.join(
        self.slap.instance_directory, instance_reference, 'etc', 'plugin', name
      )) as fh:
      promise = fh.read().strip()
    self.assertIn(content, promise)

  def assertSurykatkaBotPromise(self):
    for instance_reference in self.surykatka_dict:
      for info_dict in self.surykatka_dict[instance_reference].values():
        self.assertPromiseContent(
          instance_reference,
          info_dict['bot-promise'],
          "'report': 'bot_status'")
        self.assertPromiseContent(
          instance_reference,
          info_dict['bot-promise'],
          "'json-file': '%s'" % (info_dict['json-file'],),)

  def assertSurykatkaCron(self):
    for instance_reference in self.surykatka_dict:
      for info_dict in self.surykatka_dict[instance_reference].values():
        with open(info_dict['status-cron']) as fh:
          self.assertEqual(
            '*/2 * * * * %s' % (info_dict['status-json'],),
          fh.read().strip()
          )

  def initiateSurykatkaRun(self):
    try:
      self.slap.waitForInstance(max_retry=2)
    except Exception:
      pass

  def assertSurykatkaStatusJSON(self):
    for instance_reference in self.surykatka_dict:
      for info_dict in self.surykatka_dict[instance_reference].values():
        if os.path.exists(info_dict['json-file']):
          os.unlink(info_dict['json-file'])
        try:
          subprocess.check_call(info_dict['status-json'])
        except subprocess.CalledProcessError as e:
          self.fail('%s failed with code %s and message %s' % (
            info_dict['status-json'], e.returncode, e.output))
        with open(info_dict['json-file']) as fh:
          status_json = json.load(fh)
        self.assertIn('bot_status', status_json)


class TestEdgeBasic(EdgeMixin, SlapOSInstanceTestCase):
  surykatka_dict = {}

  def assertConnectionParameterDict(self):
    connection_parameter_dict = self.requestDefaultInstance(
      ).getConnectionParameterDict()
    # tested elsewhere
    connection_parameter_dict.pop('monitor-setup-url', None)
    # comes from instance-monitor.cfg.jinja2, not needed here
    connection_parameter_dict.pop('server_log_url', None)
    self.assertEqual(
      self.expected_connection_parameter_dict,
      connection_parameter_dict
    )

  def assertHttpQueryPromiseContent(
    self, instance_reference, name, url, content):
    hashed = 'http-query-%s-%s.py' % (
      hashlib.md5((name).encode('utf-8')).hexdigest(),
      hashlib.md5((url).encode('utf-8')).hexdigest(),
    )
    self.assertPromiseContent(instance_reference, hashed, content)

  def updateSurykatkaDict(self):
    for instance_reference in self.surykatka_dict:
      for class_ in self.surykatka_dict[instance_reference]:
        update_dict = {}
        update_dict['ini-file'] = os.path.join(
          self.slap.instance_directory, instance_reference, 'etc',
          'surykatka-%s.ini' % (class_,))
        update_dict['json-file'] = os.path.join(
          self.slap.instance_directory, instance_reference, 'srv',
          'surykatka-%s.json' % (class_,))
        update_dict['status-json'] = os.path.join(
          self.slap.instance_directory, instance_reference, 'bin',
          'surykatka-status-json-%s' % (class_,))
        update_dict['bot-promise'] = 'surykatka-bot-%s.py' % (class_,)
        update_dict['status-cron'] = os.path.join(
          self.slap.instance_directory, instance_reference, 'etc',
          'cron.d', 'surykatka-status-%s' % (class_,))
        update_dict['db_file'] = os.path.join(
          self.slap.instance_directory, instance_reference, 'srv',
          'surykatka-%s.db' % (class_,))
        self.surykatka_dict[instance_reference][class_].update(update_dict)

  @classmethod
  def getInstanceParameterDict(cls):
    return {'_': json.dumps({
      'nameserver-list': ['127.0.1.1', '127.0.1.2'],
      'check-frontend-ip-list': ['127.0.0.1', '127.0.0.2'],
      "check-maximum-elapsed-time": 5,
      "check-certificate-expiration-days": 7,
      "check-status-code": 201,
      "failure-amount": 1,
      "check-dict": {
        "path-check": {
           "url-list": [
             "https://path.example.com/path",
           ]
        },
        "domain-check": {
           "url-list": [
             "domain.example.com",
           ]
        },
        "frontend-check": {
           "url-list": [
             "https://frontend.example.com",
           ],
           "check-frontend-ip-list": ['127.0.0.3'],
        },
        "frontend-empty-check": {
           "url-list": [
             "https://frontendempty.example.com",
           ],
           "check-frontend-ip-list": [],
        },
        "status-check": {
           "url-list": [
             "https://status.example.com",
           ],
           "check-status-code": 202,
        },
        "certificate-check": {
           "url-list": [
             "https://certificate.example.com",
           ],
           "check-certificate-expiration-days": 11,
        },
        "time-check": {
           "url-list": [
             "https://time.example.com",
           ],
           "check-maximum-elapsed-time": 11,
        },
        "failure-check": {
           "url-list": [
             "https://failure.example.com",
           ],
           "failure-amount": 3,
        },
        "header-check": {
           "url-list": [
             "https://header.example.com",
           ],
           'check-http-header-dict': {"A": "AAA"},
        },
      }
    })}

  surykatka_dict = {
    'edge0': {
      5: {'expected_ini': """[SURYKATKA]
INTERVAL = 120
TIMEOUT = 7
SQLITE = %(db_file)s
NAMESERVER =
  127.0.1.1
  127.0.1.2

URL =
  http://domain.example.com
  https://certificate.example.com
  https://domain.example.com
  https://failure.example.com
  https://frontend.example.com
  https://frontendempty.example.com
  https://header.example.com
  https://path.example.com/path
  https://status.example.com
"""},
      11: {'expected_ini': """[SURYKATKA]
INTERVAL = 120
TIMEOUT = 13
SQLITE = %(db_file)s
NAMESERVER =
  127.0.1.1
  127.0.1.2

URL =
  https://time.example.com
"""},
    }
  }

  @classmethod
  def getInstanceSoftwareType(cls):
    return 'edgetest-basic'

  def assertSurykatkaPromises(self):
    self.assertHttpQueryPromiseContent(
      'edge0',
      'path-check',
      'https://path.example.com/path',
      """extra_config_dict = { 'certificate-expiration-days': '7',
  'failure-amount': '1',
  'http-header-dict': '{}',
  'ip-list': '127.0.0.1 127.0.0.2',
  'json-file': '%s',
  'maximum-elapsed-time': '5',
  'report': 'http_query',
  'status-code': '201',
  'url': 'https://path.example.com/path'}""" % (
        self.surykatka_dict['edge0'][5]['json-file'],))

    self.assertHttpQueryPromiseContent(
      'edge0',
      'domain-check',
      'https://domain.example.com',
      """extra_config_dict = { 'certificate-expiration-days': '7',
  'failure-amount': '1',
  'http-header-dict': '{}',
  'ip-list': '127.0.0.1 127.0.0.2',
  'json-file': '%s',
  'maximum-elapsed-time': '5',
  'report': 'http_query',
  'status-code': '201',
  'url': 'https://domain.example.com'}""" % (
        self.surykatka_dict['edge0'][5]['json-file'],))

    self.assertHttpQueryPromiseContent(
      'edge0',
      'domain-check',
      'http://domain.example.com',
      """extra_config_dict = { 'certificate-expiration-days': '7',
  'failure-amount': '1',
  'http-header-dict': '{}',
  'ip-list': '127.0.0.1 127.0.0.2',
  'json-file': '%s',
  'maximum-elapsed-time': '5',
  'report': 'http_query',
  'status-code': '201',
  'url': 'http://domain.example.com'}""" % (
        self.surykatka_dict['edge0'][5]['json-file'],))

    self.assertHttpQueryPromiseContent(
      'edge0',
      'frontend-check',
      'https://frontend.example.com',
      """extra_config_dict = { 'certificate-expiration-days': '7',
  'failure-amount': '1',
  'http-header-dict': '{}',
  'ip-list': '127.0.0.3',
  'json-file': '%s',
  'maximum-elapsed-time': '5',
  'report': 'http_query',
  'status-code': '201',
  'url': 'https://frontend.example.com'}""" % (
        self.surykatka_dict['edge0'][5]['json-file'],))

    self.assertHttpQueryPromiseContent(
      'edge0',
      'frontend-empty-check',
      'https://frontendempty.example.com',
      """extra_config_dict = { 'certificate-expiration-days': '7',
  'failure-amount': '1',
  'http-header-dict': '{}',
  'ip-list': '',
  'json-file': '%s',
  'maximum-elapsed-time': '5',
  'report': 'http_query',
  'status-code': '201',
  'url': 'https://frontendempty.example.com'}""" % (
        self.surykatka_dict['edge0'][5]['json-file'],))

    self.assertHttpQueryPromiseContent(
      'edge0',
      'status-check',
      'https://status.example.com',
      """extra_config_dict = { 'certificate-expiration-days': '7',
  'failure-amount': '1',
  'http-header-dict': '{}',
  'ip-list': '127.0.0.1 127.0.0.2',
  'json-file': '%s',
  'maximum-elapsed-time': '5',
  'report': 'http_query',
  'status-code': '202',
  'url': 'https://status.example.com'}""" % (
        self.surykatka_dict['edge0'][5]['json-file'],))

    self.assertHttpQueryPromiseContent(
      'edge0',
      'certificate-check',
      'https://certificate.example.com',
      """extra_config_dict = { 'certificate-expiration-days': '11',
  'failure-amount': '1',
  'http-header-dict': '{}',
  'ip-list': '127.0.0.1 127.0.0.2',
  'json-file': '%s',
  'maximum-elapsed-time': '5',
  'report': 'http_query',
  'status-code': '201',
  'url': 'https://certificate.example.com'}""" % (
        self.surykatka_dict['edge0'][5]['json-file'],))

    self.assertHttpQueryPromiseContent(
      'edge0',
      'time-check',
      'https://time.example.com',
      """extra_config_dict = { 'certificate-expiration-days': '7',
  'failure-amount': '1',
  'http-header-dict': '{}',
  'ip-list': '127.0.0.1 127.0.0.2',
  'json-file': '%s',
  'maximum-elapsed-time': '11',
  'report': 'http_query',
  'status-code': '201',
  'url': 'https://time.example.com'}""" % (
        self.surykatka_dict['edge0'][11]['json-file'],))

    self.assertHttpQueryPromiseContent(
      'edge0',
      'failure-check',
      'https://failure.example.com',
      """extra_config_dict = { 'certificate-expiration-days': '7',
  'failure-amount': '3',
  'http-header-dict': '{}',
  'ip-list': '127.0.0.1 127.0.0.2',
  'json-file': '%s',
  'maximum-elapsed-time': '5',
  'report': 'http_query',
  'status-code': '201',
  'url': 'https://failure.example.com'}""" % (
        self.surykatka_dict['edge0'][5]['json-file'],))

    self.assertHttpQueryPromiseContent(
      'edge0',
      'header-check',
      'https://header.example.com',
      """extra_config_dict = { 'certificate-expiration-days': '7',
  'failure-amount': '1',
  'http-header-dict': '{"A": "AAA"}',
  'ip-list': '127.0.0.1 127.0.0.2',
  'json-file': '%s',
  'maximum-elapsed-time': '5',
  'report': 'http_query',
  'status-code': '201',
  'url': 'https://header.example.com'}""" % (
        self.surykatka_dict['edge0'][5]['json-file'],))

  def test(self):
    # Note: Those tests do not run surykatka and do not do real checks, as
    #       this depends too much on the environment and is really hard to
    #       mock
    #       So it is possible that some bugs might slip under the radar
    #       Nevertheless the surykatka and check_surykatka_json are heavily
    #       unit tested, and configuration created by the profiles is asserted
    #       here, so it shall be enough as reasonable status
    self.initiateSurykatkaRun()
    self.assertSurykatkaStatusJSON()
    self.assertSurykatkaIni()
    self.assertSurykatkaBotPromise()
    self.assertSurykatkaPromises()
    self.assertSurykatkaCron()
    self.assertConnectionParameterDict()