import unittest

import os, sys, os.path, time, inspect
from filecmp import dircmp
from tempfile import mkdtemp
from shutil import rmtree, copy2
from zipfile import ZipFile
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))

from mozunit import MozTestRunner
from JarMaker import JarMaker

if sys.platform == "win32":
    import ctypes
    from ctypes import POINTER, WinError
    DWORD = ctypes.c_ulong
    LPDWORD = POINTER(DWORD)
    HANDLE = ctypes.c_void_p
    GENERIC_READ = 0x80000000
    FILE_SHARE_READ = 0x00000001
    OPEN_EXISTING = 3
    MAX_PATH = 260

    class FILETIME(ctypes.Structure):
        _fields_ = [("dwLowDateTime", DWORD),
                    ("dwHighDateTime", DWORD)]

    class BY_HANDLE_FILE_INFORMATION(ctypes.Structure):
        _fields_ = [("dwFileAttributes", DWORD),
                    ("ftCreationTime", FILETIME),
                    ("ftLastAccessTime", FILETIME),
                    ("ftLastWriteTime", FILETIME),
                    ("dwVolumeSerialNumber", DWORD),
                    ("nFileSizeHigh", DWORD),
                    ("nFileSizeLow", DWORD),
                    ("nNumberOfLinks", DWORD),
                    ("nFileIndexHigh", DWORD),
                    ("nFileIndexLow", DWORD)]

    # http://msdn.microsoft.com/en-us/library/aa363858
    CreateFile = ctypes.windll.kernel32.CreateFileA
    CreateFile.argtypes = [ctypes.c_char_p, DWORD, DWORD, ctypes.c_void_p,
                           DWORD, DWORD, HANDLE]
    CreateFile.restype = HANDLE

    # http://msdn.microsoft.com/en-us/library/aa364952
    GetFileInformationByHandle = ctypes.windll.kernel32.GetFileInformationByHandle
    GetFileInformationByHandle.argtypes = [HANDLE, POINTER(BY_HANDLE_FILE_INFORMATION)]
    GetFileInformationByHandle.restype = ctypes.c_int

    # http://msdn.microsoft.com/en-us/library/aa364996
    GetVolumePathName = ctypes.windll.kernel32.GetVolumePathNameA
    GetVolumePathName.argtypes = [ctypes.c_char_p, ctypes.c_char_p, DWORD]
    GetVolumePathName.restype = ctypes.c_int

    # http://msdn.microsoft.com/en-us/library/aa364993
    GetVolumeInformation = ctypes.windll.kernel32.GetVolumeInformationA
    GetVolumeInformation.argtypes = [ctypes.c_char_p, ctypes.c_char_p, DWORD,
                                     LPDWORD, LPDWORD, LPDWORD, ctypes.c_char_p,
                                     DWORD]
    GetVolumeInformation.restype = ctypes.c_int

def symlinks_supported(path):
    if sys.platform == "win32":
        # Add 1 for a trailing backslash if necessary, and 1 for the terminating
        # null character.
        volpath = ctypes.create_string_buffer(len(path) + 2)
        rv = GetVolumePathName(path, volpath, len(volpath))
        if rv == 0:
            raise WinError()

        fsname = ctypes.create_string_buffer(MAX_PATH + 1)
        rv = GetVolumeInformation(volpath, None, 0, None, None, None, fsname,
                                  len(fsname))
        if rv == 0:
            raise WinError()

        # Return true only if the fsname is NTFS
        return fsname.value == "NTFS"
    else:
        return True

def _getfileinfo(path):
    """Return information for the given file. This only works on Windows."""
    fh = CreateFile(path, GENERIC_READ, FILE_SHARE_READ, None, OPEN_EXISTING, 0, None)
    if fh is None:
        raise WinError()
    info = BY_HANDLE_FILE_INFORMATION()
    rv = GetFileInformationByHandle(fh, info)
    if rv == 0:
        raise WinError()
    return info

def is_symlink_to(dest, src):
    if sys.platform == "win32":
        # Check if both are on the same volume and have the same file ID
        destinfo = _getfileinfo(dest)
        srcinfo = _getfileinfo(src)
        return (destinfo.dwVolumeSerialNumber == srcinfo.dwVolumeSerialNumber and
                destinfo.nFileIndexHigh == srcinfo.nFileIndexHigh and
                destinfo.nFileIndexLow == srcinfo.nFileIndexLow)
    else:
        # Read the link and check if it is correct
        if not os.path.islink(dest):
            return False
        target = os.path.abspath(os.readlink(dest))
        abssrc = os.path.abspath(src)
        return target == abssrc

class _TreeDiff(dircmp):
    """Helper to report rich results on difference between two directories.
    """
    def _fillDiff(self, dc, rv, basepath="%s"):
        rv['right_only'] += map(lambda l: basepath % l, dc.right_only)
        rv['left_only'] += map(lambda l: basepath % l, dc.left_only)
        rv['diff_files'] += map(lambda l: basepath % l, dc.diff_files)
        rv['funny'] += map(lambda l: basepath % l, dc.common_funny)
        rv['funny'] += map(lambda l: basepath % l, dc.funny_files)
        for subdir, _dc in dc.subdirs.iteritems():
            self._fillDiff(_dc, rv, basepath % (subdir + "/%s"))
    def allResults(self, left, right):
        rv = {'right_only':[], 'left_only':[],
              'diff_files':[], 'funny': []}
        self._fillDiff(self, rv)
        chunks = []
        if rv['right_only']:
            chunks.append('%s only in %s' % (', '.join(rv['right_only']),
                                            right))
        if rv['left_only']:
            chunks.append('%s only in %s' % (', '.join(rv['left_only']),
                                            left))
        if rv['diff_files']:
            chunks.append('%s differ' % ', '.join(rv['diff_files']))
        if rv['funny']:
            chunks.append("%s don't compare" % ', '.join(rv['funny']))
        return '; '.join(chunks)

class TestJarMaker(unittest.TestCase):
    """
    Unit tests for JarMaker.py
    """
    debug = True # set to True to debug failing tests on disk
    def setUp(self):
        self.tmpdir = mkdtemp()
        self.srcdir = os.path.join(self.tmpdir, 'src')
        os.mkdir(self.srcdir)
        self.builddir = os.path.join(self.tmpdir, 'build')
        os.mkdir(self.builddir)
        self.refdir = os.path.join(self.tmpdir, 'ref')
        os.mkdir(self.refdir)
        self.stagedir = os.path.join(self.tmpdir, 'stage')
        os.mkdir(self.stagedir)

    def tearDown(self):
        if self.debug:
            print self.tmpdir
        else:
            rmtree(self.tmpdir)

    def _jar_and_compare(self, *args, **kwargs):
        jm = JarMaker(outputFormat='jar')
        kwargs['jardir'] = os.path.join(self.builddir, 'chrome')
        if 'topsourcedir' not in kwargs:
            kwargs['topsourcedir'] = self.srcdir
        jm.makeJars(*args, **kwargs)
        cwd = os.getcwd()
        os.chdir(self.builddir)
        try:
            # expand build to stage
            for path, dirs, files in os.walk('.'):
                stagedir = os.path.join(self.stagedir, path)
                if not os.path.isdir(stagedir):
                    os.mkdir(stagedir)
                for file in files:
                    if file.endswith('.jar'):
                        # expand jar
                        stagepath = os.path.join(stagedir, file)
                        os.mkdir(stagepath)
                        zf = ZipFile(os.path.join(path, file))
                        # extractall is only in 2.6, do this manually :-(
                        for entry_name in zf.namelist():
                            segs = entry_name.split('/')
                            fname = segs.pop()
                            dname = os.path.join(stagepath, *segs)
                            if not os.path.isdir(dname):
                                os.makedirs(dname)
                            if not fname:
                                # directory, we're done
                                continue
                            _c = zf.read(entry_name)
                            open(os.path.join(dname, fname), 'wb').write(_c)
                        zf.close()
                    else:
                        copy2(os.path.join(path, file), stagedir)
            # compare both dirs
            os.chdir('..')
            td = _TreeDiff('ref', 'stage')
            return td.allResults('reference', 'build')
        finally:
            os.chdir(cwd)

    def _create_simple_setup(self):
        # create src content
        jarf = open(os.path.join(self.srcdir, 'jar.mn'), 'w')
        jarf.write('''test.jar:
 dir/foo (bar)
''')
        jarf.close()
        open(os.path.join(self.srcdir,'bar'),'w').write('content\n')
        # create reference
        refpath = os.path.join(self.refdir, 'chrome', 'test.jar', 'dir')
        os.makedirs(refpath)
        open(os.path.join(refpath, 'foo'), 'w').write('content\n')        

    def test_a_simple_jar(self):
        '''Test a simple jar.mn'''
        self._create_simple_setup()
        # call JarMaker
        rv = self._jar_and_compare((os.path.join(self.srcdir,'jar.mn'),),
                                   tuple(),
                                   sourcedirs = [self.srcdir])
        self.assertTrue(not rv, rv)

    def test_a_simple_symlink(self):
        '''Test a simple jar.mn with a symlink'''
        if not symlinks_supported(self.srcdir):
            return

        self._create_simple_setup()
        jm = JarMaker(outputFormat='symlink')
        kwargs = {
            'sourcedirs': [self.srcdir],
            'topsourcedir': self.srcdir,
            'jardir': os.path.join(self.builddir, 'chrome'),
        }
        jm.makeJars((os.path.join(self.srcdir,'jar.mn'),), tuple(), **kwargs)
        # All we do is check that srcdir/bar points to builddir/chrome/test/dir/foo
        srcbar = os.path.join(self.srcdir, 'bar')
        destfoo = os.path.join(self.builddir, 'chrome', 'test', 'dir', 'foo')
        self.assertTrue(is_symlink_to(destfoo, srcbar),
                        "%s is not a symlink to %s" % (destfoo, srcbar))

    def test_k_multi_relative_jar(self):
        '''Test the API for multiple l10n jars, with different relative paths'''
        # create app src content
        def _mangle(relpath):
            'method we use to map relpath to srcpaths'
            return os.path.join(self.srcdir, 'other-' + relpath)
        jars = []
        for relpath in ('foo', 'bar'):
            ldir = os.path.join(self.srcdir, relpath, 'locales')
            os.makedirs(ldir)
            jp = os.path.join(ldir, 'jar.mn')
            jars.append(jp)
            open(jp, 'w').write('''ab-CD.jar:
% locale app ab-CD %app
  app/''' + relpath + ' (%' + relpath + ''')
''')
            ldir = _mangle(relpath)
            os.mkdir(ldir)
            open(os.path.join(ldir, relpath), 'w').write(relpath+" content\n")
        # create reference
        mf = open(os.path.join(self.refdir, 'chrome.manifest'), 'w')
        mf.write('manifest chrome/ab-CD.manifest\n')
        mf.close()

        chrome_ref = os.path.join(self.refdir, 'chrome')
        os.mkdir(chrome_ref)
        mf = open(os.path.join(chrome_ref, 'ab-CD.manifest'), 'wb')
        mf.write('locale app ab-CD jar:ab-CD.jar!/app\n')
        mf.close()
        ldir = os.path.join(chrome_ref, 'ab-CD.jar', 'app')
        os.makedirs(ldir)
        for relpath in ('foo', 'bar'):
            open(os.path.join(ldir, relpath), 'w').write(relpath+" content\n")
        # call JarMaker
        difference = self._jar_and_compare(jars,
                                           (_mangle,),
                                           sourcedirs = [])
        self.assertTrue(not difference, difference)

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