#!/usr/bin/python3

#
# test.py
# for babackup
#
# Copyright (C) 2024 by John Heidemann <johnh@isi.edu>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License,
# version 2, as published by the Free Software Foundation.
# 
# 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 argparse
import sys
import pdb
# pdb.set_trace()
import os
import shutil
import re
import subprocess
import types
import datetime
import platform

test_client_conf = """backups:
- mode: local
  name: new
  path:
  - !!TESTDIR/clientdir/
  relative: false
  userserverpath: !!TESTDIR/server
"""
test_client_conf_alt = """backups:
- mode: local
  name: new
  path: [!!TESTDIR/clientdir/]
  userserverpath: !!TESTDIR/server
  relative: false
"""

test_server_conf = """backups:
- mode: local
  name: new
  server_path: !!TESTDIR/server
"""

test_server_conf_alt = """backups:
- {mode: local, name: new, server_path: !!TESTDIR/server}
"""

test_file_contents = "test file contents\n"

def test_populate_dirs(path):
    for timestamp in range(1704013200, 1696237200, -86400):
        os.makedirs(path + "/" + datetime.datetime.fromtimestamp(timestamp, datetime.timezone.utc).isoformat(timespec='minutes')) 


tests = [
    {
        "name": "babackup-new-test",
        'cmds': ["!!BABACKUP -c !!TESTDIR/client.yaml --name=new --new-mode=local --new-path=!!TESTDIR/clientdir/ --new-server=!!TESTDIR/server"],
        'eval': lambda test: test.filematches_patched(f"{test.dir}/client.yaml", [ test_client_conf, test_client_conf_alt ]),
    },
    {
        "name": "babackup_server",
        'cmds': ["!!BABACKUP -c !!TESTDIR/client.yaml --name=new --new-mode=local --new-path=!!TESTDIR/clientdir/ --new-server=!!TESTDIR/server",
                 "!!BABACKUP_SERVER -c !!TESTDIR/server.yaml --name=new --new-mode=local --new-server-path=!!TESTDIR/server"],
        'eval': lambda test: test.filematches_patched(f"{test.dir}/server.yaml", [ test_server_conf, test_server_conf_alt ]),
    },
    {
        "name": "rsync-once",
        'cmds': ["!!BABACKUP -c !!TESTDIR/client.yaml --name=new --new-mode=local --new-path=!!TESTDIR/clientdir/ --new-server=!!TESTDIR/server",
                 lambda test: os.mkdir(test.patch_string("!!TESTDIR/clientdir")),
                 lambda test: test.writepatched_to_file(test_file_contents, test.patch_string("!!TESTDIR/clientdir/file")),
                 "!!BABACKUP_SERVER -c !!TESTDIR/server.yaml --name=new --new-mode=local --new-server-path=!!TESTDIR/server",
                 "!!BABACKUP -c !!TESTDIR/client.yaml"],
        'eval': lambda test: test.filematches_patched(f"{test.dir}/server/current/data/file", [ test_file_contents ]),
    },
    {
        "name": "rsync-once-server",
        'cmds': ["!!BABACKUP -c !!TESTDIR/client.yaml --name=new --new-mode=local --new-path=!!TESTDIR/clientdir/ --new-server=!!TESTDIR/server",
                 lambda test: os.mkdir(test.patch_string("!!TESTDIR/clientdir")),
                 lambda test: test.writepatched_to_file(test_file_contents, test.patch_string("!!TESTDIR/clientdir/file")),
                 "!!BABACKUP_SERVER -c !!TESTDIR/server.yaml --name=new --new-mode=local --new-server-path=!!TESTDIR/server",
                 "!!BABACKUP -c !!TESTDIR/client.yaml",
                 "!!BABACKUP_SERVER -c !!TESTDIR/server.yaml"],
        'eval': lambda test: test.filematches_patched(f"{test.dir}/server/current/last/data/file", [ test_file_contents ]),
    },
    {
        "name": "rsync-twice-server",
        'cmds': ["!!BABACKUP -c !!TESTDIR/client.yaml --name=new --new-mode=local --new-path=!!TESTDIR/clientdir/ --new-server=!!TESTDIR/server",
                 lambda test: os.mkdir(test.patch_string("!!TESTDIR/clientdir")),
                 lambda test: test.writepatched_to_file(test_file_contents, test.patch_string("!!TESTDIR/clientdir/file")),
                 "!!BABACKUP_SERVER -c !!TESTDIR/server.yaml --name=new --new-mode=local --new-server-path=!!TESTDIR/server",
                 "!!BABACKUP -c !!TESTDIR/client.yaml",
                 "!!BABACKUP_SERVER -c !!TESTDIR/server.yaml",
                 "!!BABACKUP -c !!TESTDIR/client.yaml -f",
                 "!!BABACKUP_SERVER -c !!TESTDIR/server.yaml"],
        'eval': lambda test: test.filematches_patched(f"{test.dir}/server/current/last/data/file", [ test_file_contents ]) and os.path.isdir(f"{test.dir}/server/archive"),
    },
    {
        "name": "server-cleanup",
        'cmds': ["!!BABACKUP -c !!TESTDIR/client.yaml --name=new --new-mode=local --new-path=!!TESTDIR/clientdir/ --new-server=!!TESTDIR/server",
                 lambda test: os.mkdir(test.patch_string("!!TESTDIR/clientdir")),
                 lambda test: test.writepatched_to_file(test_file_contents, test.patch_string("!!TESTDIR/clientdir/file")),
                 "!!BABACKUP_SERVER -c !!TESTDIR/server.yaml --name=new --new-mode=local --new-server-path=!!TESTDIR/server", 
                 lambda test: test_populate_dirs(f"{test.dir}/server/archive"),
                 "!!BABACKUP -c !!TESTDIR/client.yaml",
                 "!!BABACKUP_SERVER -c !!TESTDIR/server.yaml --check-archive"],
        'eval': lambda test: os.path.isdir(f"{test.dir}/server/archive/2023-12-31T09:00+00:00") and os.path.isdir(f"{test.dir}/server/archive/2023-12-21T09:00+00:00") and not os.path.isdir(f"{test.dir}/server/archive/2023-12-20T09:00+00:00") and not os.path.isdir(f"{test.dir}/server/archive/2023-12-16T09:00+00:00") and os.path.isdir(f"{test.dir}/server/archive/2023-12-15T09:00+00:00") and not os.path.isdir(f"{test.dir}/server/archive/2023-12-14T09:00+00:00"),
    },
    {
        "name": "server-with-secondary",
        'cmds': ["!!BABACKUP -c !!TESTDIR/client.yaml --name=new --new-mode=local --new-path=!!TESTDIR/clientdir/ --new-server=!!TESTDIR/server",
                 "!!BABACKUP_SERVER -c !!TESTDIR/server.yaml --name=new --new-mode=local --new-server-path=!!TESTDIR/server --new-secondary-path=!!TESTDIR/secondary",
                 lambda test: os.mkdir(test.patch_string("!!TESTDIR/clientdir")),
                 lambda test: test.writepatched_to_file(test_file_contents, test.patch_string("!!TESTDIR/clientdir/file")),
                 "!!BABACKUP -c !!TESTDIR/client.yaml",
                 "!!BABACKUP_SERVER -c !!TESTDIR/server.yaml",
],
        'eval': lambda test: os.path.isfile(f"{test.dir}/secondary/current/last/end"),
    },
]


def subprocess_run_capture_output(cmd):
    """backwards compatible subprocess.run for capture_output"""
    (major, minor, _) = platform.python_version_tuple()
    if int(major) == 3 and int(minor) >= 7:
        return subprocess.run(cmd, capture_output = True, encoding = 'utf-8')
    else:
        return subprocess.run(cmd, encoding = 'utf-8', stdout = subprocess.PIPE, stderr = subprocess.PIPE)


def run_nofail(args):
    """run a system command given as ARGS and abort on failure"""
    result = subprocess_run_capture_output(args)
    if result.returncode != 0:
        sys.exit("command failed: " + " ".join(args) + "\nSTDOUT:\n" + result.stdout + "\nSTDERR:\n" + result.stderr + "\n")



class Test:
    def __init__(self, test, babackup_path, babackup_server_path, testno, verbose):
        self.test = test
        self.babackup_path = babackup_path
        self.babackup_server_path = babackup_server_path
        self.testno = testno
        self.verbose = verbose


    def patch_string(self, s):
        """patch a string S for magic variables"""
        s = re.sub(r'!!TESTDIR', self.dir, s)
        s = re.sub(r'!!BABACKUP_SERVER', self.babackup_server_path, s)
        s = re.sub(r'!!BABACKUP', self.babackup_path, s)
        return s


    def prep_command(self, cmd):
        """split a string CMD and expand macros"""
        args = self.patch_string(cmd).split(" ")
        return args

    
    def writepatched_to_file(self, contents, file):
        """take CONTENTS, patch them, and write them to FILE"""
        contents_patched = self.patch_string(contents)
        with open(file, "w") as file_stream:
            file_stream.write(contents_patched)

            
    def filematches_patched(self, file, goal_contentses):
        """check file the contents of FILE matches GOAL_CONTENTS after contenst are patched"""
        with open(file, "r") as file_stream:
            file_contents = file_stream.read()
        goal_contentses_patched = list(map(lambda s: self.patch_string(s), goal_contentses))
        if not file_contents in goal_contentses_patched:
            print(f"{self.test['name']}: file {file} contents do not match\nHAVE:\n{file_contents}\nWANT:\n" + ("OR:\n".join(goal_contentses)))
            return False
        return True

    def log(self, s):
        """maybe log S"""
        if self.verbose > 0:
            print(s)

    
    def run(self):
        """run a partircular test and check its correctness"""
        print(f"test: {self.test['name']} going")
        self.dir = f"TEST/{os.getpid()}_{self.testno}~"
        os.makedirs(self.dir)

        success = True
        if self.test.get('prepare_cmd'):
            run_nofail(self.test['prepare_cmd'])
        if self.test.get('cmds'):
            for cmd in self.test['cmds']:
                if type(cmd) is str:
                    self.log(f"test.py: run cmd: {cmd}")
                    run_nofail(self.prep_command(cmd))
                elif type(cmd) is types.LambdaType:
                    self.log(f"test.py: run lambda")
                    cmd(self)
                else:
                    sys.exit("test.py: internal error, unknown type of cmd")
        if self.test.get('eval'):
            success = self.test['eval'](self)

        if success:
            print(f"\t{self.test['name']} OK")
            if self.test.get('keep', False):
                print(f"   output in {self.dir}")
            else:
                shutil.rmtree(self.dir)
        else:
            print(f"\t{self.test['name']} FAIL: look in {self.dir}")
    
    

class Program:
    testno = 0

    def __init__(self):
        # if assertion_fails:
        #     raise Exception("Assertion failed")
        # or better sys.exit("Assertion failed")
        args = self.parse_args()
        if args.tests is None or len(args.tests) == 0:
            self.run_tests(lambda test: True)
        else:
            self.run_tests(lambda test: test['name'] in args.tests)

    def parse_args(self):
        parser = argparse.ArgumentParser(description = 'what my program does', epilog="""
long description

        """)
        # see https://docs.python.org/3/library/argparse.html
        #  ArgumentParser.add_argument(name or flags...[, action][, nargs][, const][, default][, type][, choices][, required][, help][, metavar][, dest])

        #  parser.add_argument('--focus', help='focus on a given TARGET', choices=['us', 'nynj', 'coverage'], default='us')
        #  parser.add_argument('--output', '-o', help='output FILE')
        #  parser.add_argument('--duty-cycle', help='duty cycle (a float)', type=float)
        #  parser.add_argument('--type', '-t', choices=['pdf', 'png'], help='type of output (pdf or png)', default = 'pdf')
        #  parser.add_argument('--day', type=int, help='day to plot', default = None)
        parser.add_argument('--debug', help='debugging mode', action='store_true', default=False)
        parser.add_argument('--verbose', '-v', help='select verbosity', action='count', default=0)
        parser.add_argument('--keep', '-k', help='keep temporary files', action='count')
        parser.add_argument('--babackup-path', help='what babackup to run', default="./babackup")
        parser.add_argument('--babackup_server-path', help='what babackup_server to run', default='./babackup_server')
        parser.add_argument('tests', help='names of test to run', action='append', nargs = '*')
        args = parser.parse_args()
        # for some reason, it is wrapped in a list
        args.tests = args.tests[0]
        self.keep = args.keep
        self.debug = args.debug
        self.verbose = args.verbose
        self.babackup_path = args.babackup_path
        self.babackup_server_path = args.babackup_server_path
        return args

    def run_tests(self, check):
        """run all tests in TEST/*.cfg"""
        for test in tests:
            if check(test):
                if self.keep:
                    test['keep'] = True
                self.run_test(test)

    def run_test(self, test_hash):
        """run a single test"""

        test = Test(test_hash, self.babackup_path, self.babackup_server_path, self.testno, self.verbose)
        test.run()
        self.testno += 1


if __name__ == '__main__':
    Program()
    sys.exit(0)

