#!/usr/bin/env python


import contextlib
import io
import json
import os
import tempfile
import unittest

from stone import __version__
from stone.cli import (
    _actual_outputs,
    _cmdline_parser,
    _load_expected_output_manifest,
    _recursive_stone_specs,
    _validate_expected_output_manifest,
)
from stone.cli_helpers import parse_route_attr_filter


class MockRoute():
    """Used to test filtering on a route's attrs."""

    def __init__(self, attrs):
        self.attrs = attrs


class TestCLI(unittest.TestCase):

    def test_parse_route_attr_filter(self):

        _, errs = parse_route_attr_filter('*=3')
        self.assertNotEqual(len(errs), 0)

        _, errs = parse_route_attr_filter('test')
        self.assertEqual(len(errs), 1)
        self.assertEqual(errs[0], 'Unexpected end of expression.')

        _, errs = parse_route_attr_filter('hide=true)')
        self.assertNotEqual(len(errs), 0)

        _, errs = parse_route_attr_filter('(hide=true')
        self.assertNotEqual(len(errs), 0)

        _, errs = parse_route_attr_filter('hide=true and and size=1')
        self.assertNotEqual(len(errs), 0)

        # Test bool
        expr, errs = parse_route_attr_filter('hide=true')
        self.assertEqual(len(errs), 0)
        self.assertTrue(expr.eval(MockRoute({'hide': True})))
        self.assertFalse(expr.eval(MockRoute({'hide': 'true'})))

        # Test int
        expr, errs = parse_route_attr_filter('level=1')
        self.assertEqual(len(errs), 0)
        self.assertTrue(expr.eval(MockRoute({'level': 1})))
        self.assertFalse(expr.eval(MockRoute({'level': 2})))
        self.assertFalse(expr.eval(MockRoute({'level': '1'})))
        self.assertFalse(expr.eval(MockRoute({})))

        # Test float
        expr, errs = parse_route_attr_filter('f=1.25')
        self.assertEqual(len(errs), 0)
        self.assertTrue(expr.eval(MockRoute({'f': 1.25})))
        self.assertFalse(expr.eval(MockRoute({'f': 3})))
        self.assertFalse(expr.eval(MockRoute({'f': '1.25'})))
        self.assertFalse(expr.eval(MockRoute({})))

        # Test string
        expr, errs = parse_route_attr_filter('status="alpha"')
        self.assertEqual(len(errs), 0)
        self.assertTrue(expr.eval(MockRoute({'status': 'alpha'})))
        self.assertFalse(expr.eval(MockRoute({'status': 'beta'})))
        self.assertFalse(expr.eval(MockRoute({'status': 0})))
        self.assertFalse(expr.eval(MockRoute({})))

        # Test null
        expr, errs = parse_route_attr_filter('status=null')
        self.assertEqual(len(errs), 0)
        self.assertTrue(expr.eval(MockRoute({'status': None})))
        self.assertFalse(expr.eval(MockRoute({'status': 'beta'})))
        self.assertFalse(expr.eval(MockRoute({'status': 0})))
        self.assertTrue(expr.eval(MockRoute({})))

        # Test conjunction: or
        expr, errs = parse_route_attr_filter('a=1 or b=1')
        self.assertEqual(len(errs), 0)
        self.assertTrue(expr.eval(MockRoute({'a': 1})))
        self.assertTrue(expr.eval(MockRoute({'b': 1})))
        self.assertTrue(expr.eval(MockRoute({'a': 1, 'b': 1})))
        self.assertTrue(expr.eval(MockRoute({'a': 1, 'b': 10})))
        self.assertFalse(expr.eval(MockRoute({'a': '0', 'b': 0})))
        self.assertFalse(expr.eval(MockRoute({'a': 0})))
        self.assertFalse(expr.eval(MockRoute({})))

        # Test conjunction: and
        expr, errs = parse_route_attr_filter('a=1 and b=1')
        self.assertEqual(len(errs), 0)
        self.assertTrue(expr.eval(MockRoute({'a': 1, 'b': 1})))
        self.assertFalse(expr.eval(MockRoute({'a': 1})))
        self.assertFalse(expr.eval(MockRoute({'b': 1})))
        self.assertFalse(expr.eval(MockRoute({'a': 1, 'b': 10})))
        self.assertFalse(expr.eval(MockRoute({'a': '0', 'b': 0})))
        self.assertFalse(expr.eval(MockRoute({'a': 0})))
        self.assertFalse(expr.eval(MockRoute({})))

        # Test multiple conjunctions
        expr, errs = parse_route_attr_filter('a=1 or a=2 or a=3')
        self.assertEqual(len(errs), 0)
        self.assertTrue(expr.eval(MockRoute({'a': 1})))
        self.assertTrue(expr.eval(MockRoute({'a': 2})))
        self.assertTrue(expr.eval(MockRoute({'a': 3})))
        self.assertFalse(expr.eval(MockRoute({'a': 4})))

        # Test "and" has higher precendence than "or"
        expr, errs = parse_route_attr_filter('a=1 or a=2 and b=3 and c=4')
        self.assertEqual(len(errs), 0)
        self.assertTrue(expr.eval(MockRoute({'a': 1})))
        self.assertFalse(expr.eval(MockRoute({'a': 2})))
        self.assertTrue(expr.eval(MockRoute({'a': 2, 'b': 3, 'c': 4})))
        self.assertTrue(expr.eval(MockRoute({'a': 1, 'b': 3, 'c': 4})))
        self.assertFalse(expr.eval(MockRoute({'a': 0, 'b': 3, 'c': 4})))

        expr, errs = parse_route_attr_filter('a=2 and b=3 and c=4 or a=1')
        self.assertEqual(len(errs), 0)
        self.assertTrue(expr.eval(MockRoute({'a': 1})))
        self.assertFalse(expr.eval(MockRoute({'a': 2})))
        self.assertTrue(expr.eval(MockRoute({'a': 2, 'b': 3, 'c': 4})))
        self.assertTrue(expr.eval(MockRoute({'a': 1, 'b': 3, 'c': 4})))
        self.assertFalse(expr.eval(MockRoute({'a': 0, 'b': 3, 'c': 4})))

        # Test parentheses for overriding precedence
        expr, errs = parse_route_attr_filter('(a=1 or a=2) and b=3 and c=4')
        self.assertEqual(len(errs), 0)
        self.assertTrue(expr.eval(MockRoute({'a': 1, 'b': 3, 'c': 4})))
        self.assertTrue(expr.eval(MockRoute({'a': 2, 'b': 3, 'c': 4})))
        self.assertFalse(expr.eval(MockRoute({'a': 1})))
        self.assertFalse(expr.eval(MockRoute({'a': 1, 'b': 3})))

    def test_actual_outputs(self):
        with tempfile.TemporaryDirectory() as output_root:
            os.makedirs(os.path.join(output_root, 'nested'))
            with open(os.path.join(output_root, 'Generated.py'), 'w', encoding='utf-8'):
                pass
            with open(
                    os.path.join(output_root, 'nested', 'Generated.swift'),
                    'w',
                    encoding='utf-8'):
                pass

            self.assertEqual(
                _actual_outputs(output_root),
                ['Generated.py', 'nested/Generated.swift'])

    def test_recursive_stone_specs(self):
        with tempfile.TemporaryDirectory() as spec_root:
            os.makedirs(os.path.join(spec_root, 'nested'))
            with open(os.path.join(spec_root, 'top.stone'), 'w', encoding='utf-8'):
                pass
            with open(os.path.join(spec_root, 'nested', 'child.stone'), 'w', encoding='utf-8'):
                pass
            with open(os.path.join(spec_root, 'nested', 'ignored.txt'), 'w', encoding='utf-8'):
                pass

            self.assertEqual(
                _recursive_stone_specs(spec_root),
                [
                    os.path.join(spec_root, 'nested', 'child.stone'),
                    os.path.join(spec_root, 'top.stone'),
                ])

    def test_load_expected_output_manifest(self):
        with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8') as manifest_file:
            json.dump(['b.py', 'a.py'], manifest_file)
            manifest_file.flush()

            self.assertEqual(_load_expected_output_manifest(manifest_file.name), ['a.py', 'b.py'])

    def test_validate_expected_output_manifest(self):
        _validate_expected_output_manifest(['a.py'], ['a.py'])

        with self.assertRaises(SystemExit):
            _validate_expected_output_manifest(['a.py'], ['b.py'])

    def test_version_flag(self):
        # --version prints the version and exits 0, without requiring the
        # otherwise-mandatory positional arguments.
        out = io.StringIO()
        with self.assertRaises(SystemExit) as cm, contextlib.redirect_stdout(out):
            _cmdline_parser.parse_args(['--version'])
        self.assertEqual(cm.exception.code, 0)
        self.assertIn(__version__, out.getvalue())


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