# Copyright (C) 2018 Google 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.

"""Tests for the core module."""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from fire import core
from fire import test_components as tc
from fire import testutils
from fire import trace
import mock

import six


class CoreTest(testutils.BaseTestCase):

  def testOneLineResult(self):
    self.assertEqual(core._OneLineResult(1), '1')  # pylint: disable=protected-access
    self.assertEqual(core._OneLineResult('hello'), 'hello')  # pylint: disable=protected-access
    self.assertEqual(core._OneLineResult({}), '{}')  # pylint: disable=protected-access
    self.assertEqual(core._OneLineResult({'x': 'y'}), '{"x": "y"}')  # pylint: disable=protected-access

  def testOneLineResultCircularRef(self):
    circular_reference = tc.CircularReference()
    self.assertEqual(core._OneLineResult(circular_reference.create()),  # pylint: disable=protected-access
                     "{'y': {...}}")

  @mock.patch('fire.interact.Embed')
  def testInteractiveMode(self, mock_embed):
    core.Fire(tc.TypedProperties, command=['alpha'])
    self.assertFalse(mock_embed.called)
    core.Fire(tc.TypedProperties, command=['alpha', '--', '-i'])
    self.assertTrue(mock_embed.called)

  @mock.patch('fire.interact.Embed')
  def testInteractiveModeFullArgument(self, mock_embed):
    core.Fire(tc.TypedProperties, command=['alpha', '--', '--interactive'])
    self.assertTrue(mock_embed.called)

  @mock.patch('fire.interact.Embed')
  def testInteractiveModeVariables(self, mock_embed):
    core.Fire(tc.WithDefaults, command=['double', '2', '--', '-i'])
    self.assertTrue(mock_embed.called)
    (variables, verbose), unused_kwargs = mock_embed.call_args
    self.assertFalse(verbose)
    self.assertEqual(variables['result'], 4)
    self.assertIsInstance(variables['self'], tc.WithDefaults)
    self.assertIsInstance(variables['trace'], trace.FireTrace)

  @mock.patch('fire.interact.Embed')
  def testInteractiveModeVariablesWithName(self, mock_embed):
    core.Fire(tc.WithDefaults,
              command=['double', '2', '--', '-i', '-v'], name='D')
    self.assertTrue(mock_embed.called)
    (variables, verbose), unused_kwargs = mock_embed.call_args
    self.assertTrue(verbose)
    self.assertEqual(variables['result'], 4)
    self.assertIsInstance(variables['self'], tc.WithDefaults)
    self.assertEqual(variables['D'], tc.WithDefaults)
    self.assertIsInstance(variables['trace'], trace.FireTrace)

  # TODO(dbieber): Use parameterized tests to break up repetitive tests.
  def testHelpWithClass(self):
    with self.assertRaisesFireExit(0, 'SYNOPSIS.*ARG1'):
      core.Fire(tc.InstanceVars, command=['--', '--help'])
    with self.assertRaisesFireExit(0, 'INFO:.*SYNOPSIS.*ARG1'):
      core.Fire(tc.InstanceVars, command=['--help'])
    with self.assertRaisesFireExit(0, 'INFO:.*SYNOPSIS.*ARG1'):
      core.Fire(tc.InstanceVars, command=['-h'])

  def testHelpWithMember(self):
    with self.assertRaisesFireExit(0, 'SYNOPSIS.*capitalize'):
      core.Fire(tc.TypedProperties, command=['gamma', '--', '--help'])
    with self.assertRaisesFireExit(0, 'INFO:.*SYNOPSIS.*capitalize'):
      core.Fire(tc.TypedProperties, command=['gamma', '--help'])
    with self.assertRaisesFireExit(0, 'INFO:.*SYNOPSIS.*capitalize'):
      core.Fire(tc.TypedProperties, command=['gamma', '-h'])
    with self.assertRaisesFireExit(0, 'INFO:.*SYNOPSIS.*delta'):
      core.Fire(tc.TypedProperties, command=['delta', '--help'])
    with self.assertRaisesFireExit(0, 'INFO:.*SYNOPSIS.*echo'):
      core.Fire(tc.TypedProperties, command=['echo', '--help'])

  def testHelpOnErrorInConstructor(self):
    with self.assertRaisesFireExit(0, 'SYNOPSIS.*VALUE'):
      core.Fire(tc.ErrorInConstructor, command=['--', '--help'])
    with self.assertRaisesFireExit(0, 'INFO:.*SYNOPSIS.*VALUE'):
      core.Fire(tc.ErrorInConstructor, command=['--help'])

  def testHelpWithNamespaceCollision(self):
    # Tests cases when calling the help shortcut should not show help.
    with self.assertOutputMatches(stdout='DESCRIPTION.*', stderr=None):
      core.Fire(tc.WithHelpArg, command=['--help', 'False'])
    with self.assertOutputMatches(stdout='help in a dict', stderr=None):
      core.Fire(tc.WithHelpArg, command=['dictionary', '__help'])
    with self.assertOutputMatches(stdout='{}', stderr=None):
      core.Fire(tc.WithHelpArg, command=['dictionary', '--help'])
    with self.assertOutputMatches(stdout='False', stderr=None):
      core.Fire(tc.function_with_help, command=['False'])

  def testInvalidParameterRaisesFireExit(self):
    with self.assertRaisesFireExit(2, 'runmisspelled'):
      core.Fire(tc.Kwargs, command=['props', '--a=1', '--b=2', 'runmisspelled'])

  def testErrorRaising(self):
    # Errors in user code should not be caught; they should surface as normal.
    # This will lead to exit status code 1 for the client program.
    with self.assertRaises(ValueError):
      core.Fire(tc.ErrorRaiser, command=['fail'])

  def testFireError(self):
    error = core.FireError('Example error')
    self.assertIsNotNone(error)

  def testFireErrorMultipleValues(self):
    error = core.FireError('Example error', 'value')
    self.assertIsNotNone(error)

  def testPrintEmptyDict(self):
    with self.assertOutputMatches(stdout='{}', stderr=None):
      core.Fire(tc.EmptyDictOutput, command=['totally_empty'])
    with self.assertOutputMatches(stdout='{}', stderr=None):
      core.Fire(tc.EmptyDictOutput, command=['nothing_printable'])

  def testPrintOrderedDict(self):
    with self.assertOutputMatches(stdout=r'A:\s+A\s+2:\s+2\s+', stderr=None):
      core.Fire(tc.OrderedDictionary, command=['non_empty'])
    with self.assertOutputMatches(stdout='{}'):
      core.Fire(tc.OrderedDictionary, command=['empty'])

  def testPrintNamedTupleField(self):
    with self.assertOutputMatches(stdout='11', stderr=None):
      core.Fire(tc.NamedTuple, command=['point', 'x'])

  def testPrintNamedTupleFieldNameEqualsValue(self):
    with self.assertOutputMatches(stdout='x', stderr=None):
      core.Fire(tc.NamedTuple, command=['matching_names', 'x'])

  def testPrintNamedTupleIndex(self):
    with self.assertOutputMatches(stdout='22', stderr=None):
      core.Fire(tc.NamedTuple, command=['point', '1'])

  def testPrintSet(self):
    with self.assertOutputMatches(stdout='.*three.*', stderr=None):
      core.Fire(tc.simple_set(), command=[])

  def testPrintFrozenSet(self):
    with self.assertOutputMatches(stdout='.*three.*', stderr=None):
      core.Fire(tc.simple_frozenset(), command=[])

  def testPrintNamedTupleNegativeIndex(self):
    with self.assertOutputMatches(stdout='11', stderr=None):
      core.Fire(tc.NamedTuple, command=['point', '-2'])

  def testCallable(self):
    with self.assertOutputMatches(stdout=r'foo:\s+foo\s+', stderr=None):
      core.Fire(tc.CallableWithKeywordArgument(), command=['--foo=foo'])
    with self.assertOutputMatches(stdout=r'foo\s+', stderr=None):
      core.Fire(tc.CallableWithKeywordArgument(), command=['print_msg', 'foo'])
    with self.assertOutputMatches(stdout=r'', stderr=None):
      core.Fire(tc.CallableWithKeywordArgument(), command=[])

  def testCallableWithPositionalArgs(self):
    with self.assertRaisesFireExit(2, ''):
      # This does not give 7 since positional args are disallowed for callable
      # objects.
      core.Fire(tc.CallableWithPositionalArgs(), command=['3', '4'])

  def testStaticMethod(self):
    self.assertEqual(
        core.Fire(tc.HasStaticAndClassMethods,
                  command=['static_fn', 'alpha']),
        'alpha',
    )

  def testClassMethod(self):
    self.assertEqual(
        core.Fire(tc.HasStaticAndClassMethods,
                  command=['class_fn', '6']),
        7,
    )

  def testCustomSerialize(self):
    def serialize(x):
      if isinstance(x, list):
        return ', '.join(str(xi) for xi in x)
      if isinstance(x, dict):
        return ', '.join('{}={!r}'.format(k, v) for k, v in sorted(x.items()))
      if x == 'special':
        return ['SURPRISE!!', "I'm a list!"]
      return x

    ident = lambda x: x

    with self.assertOutputMatches(stdout='a, b', stderr=None):
      _ = core.Fire(ident, command=['[a,b]'], serialize=serialize)
    with self.assertOutputMatches(stdout='a=5, b=6', stderr=None):
      _ = core.Fire(ident, command=['{a:5,b:6}'], serialize=serialize)
    with self.assertOutputMatches(stdout='asdf', stderr=None):
      _ = core.Fire(ident, command=['asdf'], serialize=serialize)
    with self.assertOutputMatches(
        stdout="SURPRISE!!\nI'm a list!\n", stderr=None):
      _ = core.Fire(ident, command=['special'], serialize=serialize)
    with self.assertRaises(core.FireError):
      core.Fire(ident, command=['asdf'], serialize=55)

  @testutils.skipIf(six.PY2, 'lru_cache is Python 3 only.')
  def testLruCacheDecoratorBoundArg(self):
    self.assertEqual(
        core.Fire(tc.py3.LruCacheDecoratedMethod,  # pytype: disable=module-attr
                  command=['lru_cache_in_class', 'foo']), 'foo')

  @testutils.skipIf(six.PY2, 'lru_cache is Python 3 only.')
  def testLruCacheDecorator(self):
    self.assertEqual(
        core.Fire(tc.py3.lru_cache_decorated,  # pytype: disable=module-attr
                  command=['foo']), 'foo')


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