from __future__ import print_function
# Assuming you are using the mock library to ... mock things
try:
    from unittest.mock import patch, call, MagicMock, DEFAULT  # In Python 3, mock is built-in
    from io import StringIO
except ImportError:
    from mock import patch, call, MagicMock, DEFAULT  # Python 2
    from StringIO import StringIO
try:
  from collections import OrderedDict
except ImportError:
  from ordereddict import OrderedDict

from alibuild_helpers.build import doBuild, HttpRemoteSync, RsyncRemoteSync, NoRemoteSync
from alibuild_helpers.analytics import decideAnalytics, askForAnalytics, report_screenview, report_exception, report_event
from argparse import Namespace
from io import BytesIO
import sys
import os
import os.path
import re

import unittest
import traceback

class ExpectedExit(Exception):
  pass

TEST_ZLIB_RECIPE = """package: zlib
version: v1.2.3
source: https://github.com/star-externals/zlib
tag: master
---
./configure
make
make install
"""

TEST_ROOT_RECIPE = """package: ROOT
version: v6-08-30
source: https://github.com/root-mirror/root
tag: v6-08-00-patches
requires:
  - zlib
env:
  ROOT_TEST_1: "root test 1"
  ROOT_TEST_2: "root test 2"
  ROOT_TEST_3: "root test 3"
  ROOT_TEST_4: "root test 4"
  ROOT_TEST_5: "root test 5"
  ROOT_TEST_6: "root test 6"
prepend_path:
  PREPEND_ROOT_1: "prepend root 1"
  PREPEND_ROOT_2: "prepend root 2"
  PREPEND_ROOT_3: "prepend root 3"
  PREPEND_ROOT_4: "prepend root 4"
  PREPEND_ROOT_5: "prepend root 5"
  PREPEND_ROOT_6: "prepend root 6"
append_path:
  APPEND_ROOT_1: "append root 1"
  APPEND_ROOT_2: "append root 2"
  APPEND_ROOT_3: "append root 3"
  APPEND_ROOT_4: "append root 4"
  APPEND_ROOT_5: "append root 5"
  APPEND_ROOT_6: "append root 6"
---
./configure
make
make install
"""
TEST_ROOT_BUILD_HASH = "2cb80ea18ac9ec56b94c4d9a865caa1f7e03a8f0" if sys.version_info[0] < 3 else \
                       "1fc9be611d016280a2a66519923965839d0d009b"

TEST_DEFAULT_RELEASE = """package: defaults-release
version: v1
---
"""

def dummy_getstatusoutput(x):
  if re.match("/bin/bash --version", x):
    return (0, "GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin17)\nCopyright (C) 2007 Free Software Foundation, Inc.\n")
  if re.match("mkdir -p [^;]*$", x):
    return (0, "")
  if re.match("ln -snf[^;]*$", x):
    return (0, "")
  return {
      "GIT_DIR=/alidist/.git git rev-parse HEAD": (0, "6cec7b7b3769826219dfa85e5daa6de6522229a0"),
      'pip --disable-pip-version-check show alibuild | grep -e "^Version:" | sed -e \'s/.* //\'': (0, "v1.5.0"),
      'which pigz': (1, ""),
      'tar --ignore-failed-read -cvvf /dev/null /dev/zero': (0, "")
    }[x]

def dummy_getStatusOutputBash(x):
  return {
      'git ls-remote --heads /sw/MIRROR/root': (0, "87b87c4322d2a3fad315c919cb2e2dd73f2154dc\trefs/heads/master\nf7b336611753f1f4aaa94222b0d620748ae230c0\trefs/heads/v6-08-00-patches"),
      'git ls-remote --heads /sw/MIRROR/zlib': (0, "8822efa61f2a385e0bc83ca5819d608111b2168a\trefs/heads/master")
    }[x]

TIMES_ASKED = {}

def dummy_open(x, mode="r"):
  if mode == "r":
    known = {
      "/sw/BUILD/27ce49698e818e8efb56b6eff6dd785e503df341/defaults-release/.build_succeeded": (0, StringIO("0")),
      "/sw/BUILD/3e90b4e08bad439fa5f25282480d1adb9efb0c0d/zlib/.build_succeeded": (0, StringIO("0")),
      "/sw/BUILD/%s/ROOT/.build_succeeded" % TEST_ROOT_BUILD_HASH: (0, StringIO("0")),
      "/sw/osx_x86-64/defaults-release/v1-1/.build-hash": (1, StringIO("27ce49698e818e8efb56b6eff6dd785e503df341")),
      "/sw/osx_x86-64/zlib/v1.2.3-1/.build-hash": (1, StringIO("3e90b4e08bad439fa5f25282480d1adb9efb0c0d")),
      "/sw/osx_x86-64/ROOT/v6-08-30-1/.build-hash": (1, StringIO(TEST_ROOT_BUILD_HASH))
    }
    if not x in known:
      return DEFAULT
    threshold, result = known[x]
    if threshold > TIMES_ASKED.get(x, 0):
      result = None
    TIMES_ASKED[x] = TIMES_ASKED.get(x, 0) + 1
    if not result:
      raise IOError
    return result
  return StringIO()

def dummy_execute(x, mock_git_clone, mock_git_fetch, **kwds):
  s = " ".join(x) if isinstance(x, list) else x
  if re.match(".*ln -sfn.*TARS", s):
    return 0
  if re.search("^git clone --bare", s):
    mock_git_clone()
  elif re.search("&& git fetch -f --tags", s):
    mock_git_fetch()
    return 0
  return {
    "/bin/bash -e -x /sw/SPECS/osx_x86-64/defaults-release/v1-1/build.sh 2>&1": 0,
    '/bin/bash -e -x /sw/SPECS/osx_x86-64/zlib/v1.2.3-1/build.sh 2>&1': 0,
    '/bin/bash -e -x /sw/SPECS/osx_x86-64/ROOT/v6-08-30-1/build.sh 2>&1': 0,
    "git clone --bare https://github.com/star-externals/zlib /sw/MIRROR/zlib": 0,
    "git clone --bare https://github.com/root-mirror/root /sw/MIRROR/root": 0
  }[s]

def dummy_readlink(x):
  return {"/sw/TARS/osx_x86-64/defaults-release/defaults-release-v1-1.osx_x86-64.tar.gz": "../../osx_x86-64/store/27/27ce49698e818e8efb56b6eff6dd785e503df341/defaults-release-v1-1.osx_x86-64.tar.gz"}[x]

def dummy_exists(x):
  if x.endswith("alibuild_helpers/.git"):
    return False
  known = {
    "/alidist": True,
    "/sw/SPECS": False,
    "/alidist/.git": True,
    "alibuild_helpers/.git": False,
    "/sw/MIRROR/root": True,
    "/sw/MIRROR/zlib": False}
  if x in known:
    return known[x]
  return DEFAULT 

# A few errors we should handle, together with the expected result
class BuildTestCase(unittest.TestCase):
  @patch("alibuild_helpers.build.get")
  @patch("alibuild_helpers.build.execute")
  @patch("alibuild_helpers.workarea.execute")
  @patch("alibuild_helpers.build.getstatusoutput")
  @patch("alibuild_helpers.build.exists")
  @patch("alibuild_helpers.workarea.path.exists")
  @patch("alibuild_helpers.build.sys")
  @patch("alibuild_helpers.build.dieOnError")
  @patch("alibuild_helpers.build.readDefaults")
  @patch("alibuild_helpers.build.makedirs")
  @patch("alibuild_helpers.build.debug")
  @patch("alibuild_helpers.utilities.open")
  @patch("alibuild_helpers.build.open")
  @patch("alibuild_helpers.build.shutil")
  @patch("alibuild_helpers.build.glob")
  @patch("alibuild_helpers.build.readlink")
  @patch("alibuild_helpers.build.banner")
  @patch("alibuild_helpers.build.getStatusOutputBash")
  @patch("alibuild_helpers.workarea.is_writeable")
  @patch("alibuild_helpers.build.basename")
  def test_coverDoBuild(self, mock_basename, mock_is_writeable, mock_getStatusOutputBash, mock_banner,
                              mock_readlink, mock_glob, mock_shutil, mock_open, mock_utilities_open,
                              mock_debug, mock_makedirs, mock_read_defaults, mock_die, mock_sys,
                              mock_workarea_exists, mock_exists, mock_getstatusoutput,
                              mock_workarea_execute, mock_execute, mock_get):
    mock_readlink.side_effect = dummy_readlink
    mock_basename.side_effect = lambda x : "aliBuild"
    mock_glob.side_effect = lambda x : {"*": ["zlib"],
        "/sw/TARS/osx_x86-64/defaults-release/defaults-release-v1-*.osx_x86-64.tar.gz": ["/sw/TARS/osx_x86-64/defaults-release/defaults-release-v1-1.osx_x86-64.tar.gz"],
        "/sw/TARS/osx_x86-64/zlib/zlib-v1.2.3-*.osx_x86-64.tar.gz": [],
        "/sw/TARS/osx_x86-64/ROOT/ROOT-v6-08-30-*.osx_x86-64.tar.gz": [],
        "/sw/TARS/osx_x86-64/store/27/27ce49698e818e8efb56b6eff6dd785e503df341/*": [],
        "/sw/TARS/osx_x86-64/store/3e/3e90b4e08bad439fa5f25282480d1adb9efb0c0d/*": [],
        "/sw/TARS/osx_x86-64/store/%s/%s/*" % (TEST_ROOT_BUILD_HASH[0:2], TEST_ROOT_BUILD_HASH): [],
        "/sw/TARS/osx_x86-64/defaults-release/defaults-release-v1-1.osx_x86-64.tar.gz": ["../../osx_x86-64/store/27/27ce49698e818e8efb56b6eff6dd785e503df341/defaults-release-v1-1.osx_x86-64.tar.gz"],
      }[x]
    os.environ["ALIBUILD_NO_ANALYTICS"] = "1"
    mock_utilities_open.side_effect = lambda x : {
      "/alidist/root.sh": StringIO(TEST_ROOT_RECIPE),
      "/alidist/zlib.sh": StringIO(TEST_ZLIB_RECIPE),
      "/alidist/defaults-release.sh": StringIO(TEST_DEFAULT_RELEASE)
    }[x]
    mock_is_writeable.side_effect = lambda x: True
    mock_open.side_effect = dummy_open

    mock_git_clone = MagicMock(return_value=None)
    mock_git_fetch = MagicMock(return_value=None)
    mock_execute.side_effect = lambda x, **kwds: dummy_execute(x, mock_git_clone, mock_git_fetch, **kwds)
    mock_workarea_execute.side_effect = lambda x, **kwds: dummy_execute(x, mock_git_clone, mock_git_fetch, **kwds)

    mock_exists.side_effect = dummy_exists
    mock_workarea_exists.side_effect = dummy_exists
    mock_getstatusoutput.side_effect = dummy_getstatusoutput
    mock_getStatusOutputBash.side_effect = dummy_getStatusOutputBash
    mock_parser = MagicMock()
    mock_read_defaults.return_value = (OrderedDict({"package": "defaults-release", "disable": []}), "")
    args = Namespace(
      remoteStore="",
      writeStore="",
      referenceSources="/sw/MIRROR",
      docker=False,
      architecture="osx_x86-64",
      workDir="/sw",
      pkgname=["root"],
      configDir="/alidist",
      disable=[],
      defaults="release",
      jobs=2,
      preferSystem=[],
      noSystem=False,
      debug=True,
      dryRun=False,
      aggressiveCleanup=False,
      environment={},
      autoCleanup=False,
      noDevel=[],
      fetchRepos=False
    )
    mock_sys.version_info = sys.version_info

    mock_git_clone.reset_mock()
    mock_git_fetch.reset_mock()
    fmt, msg, code = doBuild(args, mock_parser)
    self.assertEqual(mock_git_clone.call_count, 1, "Expected only one call to git clone (called %d times instead)" % mock_git_clone.call_count)
    self.assertEqual(mock_git_fetch.call_count, 0, "Expected only no calls to git fetch (called %d times instead)" % mock_git_fetch.call_count)

    # Force fetching repos
    mock_git_clone.reset_mock()
    mock_git_fetch.reset_mock()
    args.fetchRepos = True
    fmt, msg, code = doBuild(args, mock_parser)
    mock_glob.assert_called_with("/sw/TARS/osx_x86-64/ROOT/ROOT-v6-08-30-*.osx_x86-64.tar.gz")
    self.assertEqual(msg, "Everything done")
    self.assertEqual(mock_git_clone.call_count, 1, "Expected only one call to git clone (called %d times instead)" % mock_git_clone.call_count)
    self.assertEqual(mock_git_fetch.call_count, 1, "Expected only one call to git fetch (called %d times instead)" % mock_git_fetch.call_count)

  @patch("alibuild_helpers.build.open")
  @patch("alibuild_helpers.build.get")
  @patch("alibuild_helpers.build.execute")
  @patch("alibuild_helpers.build.getstatusoutput")
  @patch("alibuild_helpers.build.os")
  @patch("alibuild_helpers.build.dieOnError")
  @patch("alibuild_helpers.build.warning")
  @patch("alibuild_helpers.build.error")
  def test_coverSyncs(self, mock_error, mock_warning, mock_die, mock_os, mock_getstatusoutput, mock_execute, mock_get, mock_open):
    syncers = [NoRemoteSync(),
               HttpRemoteSync(remoteStore="https://local/test", architecture="osx_x86-64", workdir="/sw", insecure=False),
               RsyncRemoteSync(remoteStore="ssh://local/test", writeStore="ssh://local/test", architecture="osx_x86-64", workdir="/sw", rsyncOptions="")]
    dummy_spec = { "package"        : "zlib",
                   "version"        : "v1.2.3",
                   "revision"       : "1",
                   "storePath"      : "/sw/path",
                   "linksPath"      : "/sw/links",
                   "tarballHashDir" : "/sw/TARS",
                   "tarballLinkDir" : "/sw/TARS" }

    class mockRequest:
      def __init__(self, j, simulate_err=False):
        self.j = j
        self.simulate_err = simulate_err
        self.status_code = 200 if j else 404
        self._bytes_left = 123456
        self.headers = { 'content-length': str(self._bytes_left) }
      def raise_for_status(self):
        return True
      def json(self):
        return self.j
      def iter_content(self, chunk_size=10):
        content = []
        if self.simulate_err:
          return content
        while self._bytes_left > 0:
          toread = min(chunk_size, self._bytes_left)
          content.append(b'x'*toread)
          self._bytes_left -= toread
        return content

    def mockGet(url, *a, **b):
      if "triggers_a_404" in url:
        return mockRequest(None)
      if dummy_spec["storePath"] in url:
        if dummy_spec["hash"].startswith("deadbeef"):
          return mockRequest([{ "name": "zlib-v1.2.3-1.slc7_x86-64.tar.gz" }])
        elif dummy_spec["hash"].startswith("baadf00d"):
          return mockRequest([{ "name": "zlib-v1.2.3-2.slc7_x86-64.tar.gz" }], simulate_err=True)
      else:
        return mockRequest([{ "name":"zlib-v1.2.3-1.slc7_x86-64.tar.gz" },
                            { "name":"zlib-v1.2.3-2.slc7_x86-64.tar.gz" }])
    mock_get.side_effect = mockGet
    mock_open.side_effect = lambda fn,fmode: BytesIO()
    mock_os.path.isfile.side_effect = lambda x: False  # file does not exist locally: force download

    for testHash in [ "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "baadf00dbaadf00dbaadf00dbaadf00dbaadf00d" ]:
      dummy_spec["hash"] = testHash
      for x in syncers:
        x.syncToLocal("zlib", dummy_spec)
        x.syncToRemote("zlib", dummy_spec)

    dummy_spec["storePath"] = "/triggers_a_404/path"
    for x in syncers:
      x.syncToLocal("zlib", dummy_spec)

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