###############################################################################
#
# Copyright (c) 2011 Ruslan Spivak
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

__author__ = 'Ruslan Spivak <ruslan.spivak@gmail.com>'

import unittest

from slimit import minify


def decorator(cls):
    def make_test_function(input, expected):

        def test_func(self):
            self.assertMinified(input, expected)

        return test_func

    for index, (input, expected) in enumerate(cls.TEST_CASES):
        func = make_test_function(input, expected)
        setattr(cls, 'test_case_%d' % index, func)

    return cls


@decorator
class MinifierTestCase(unittest.TestCase):

    def assertMinified(self, source, expected):
        minified = minify(source)
        self.maxDiff = None
        self.assertSequenceEqual(minified, expected)

    TEST_CASES = [
        ("""
        jQuery.fn = jQuery.prototype = {
                // For internal use only.
                _data: function( elem, name, data ) {
                        return jQuery.data( elem, name, data, true );
                }
        };
        """,
         'jQuery.fn=jQuery.prototype={_data:function(elem,name,data){return jQuery.data(elem,name,data,true);}};'),

        ('context = context instanceof jQuery ? context[0] : context;',
         'context=context instanceof jQuery?context[0]:context;'
         ),

        ("""
        /*
        * A number of helper functions used for managing events.
        * Many of the ideas behind this code originated from
        * Dean Edwards' addEvent library.
        */
        if ( elem && elem.parentNode ) {
                // Handle the case where IE and Opera return items
                // by name instead of ID
                if ( elem.id !== match[2] ) {
                        return rootjQuery.find( selector );
                }

                // Otherwise, we inject the element directly into the jQuery object
                this.length = 1;
                this[0] = elem;
        }
        """,

         'if(elem&&elem.parentNode){if(elem.id!==match[2])return rootjQuery.find(selector);this.length=1;this[0]=elem;}'
         ),

        ("""
        var a = function( obj ) {
                for ( var name in obj ) {
                        return false;
                }
                return true;
        };
        """,
         'var a=function(obj){for(var name in obj)return false;return true;};'
         ),

        ("""
        x = "string", y = 5;

        (x = 5) ? true : false;

        for (p in obj)
        ;

        if (true)
          val = null;
        else
          val = false;

        """,
         'x="string",y=5;(x=5)?true:false;for(p in obj);if(true)val=null;else val=false;'
         ),

        # for loops + empty statement in loop body
        ("""
        for (x = 0; true; x++)
        ;
        for (; true; x++)
        ;
        for (x = 0, y = 5; true; x++)
        ;

        y = (x + 5) * 20;

        """,
         'for(x=0;true;x++);for(;true;x++);for(x=0,y=5;true;x++);y=(x+5)*20;'),


        # unary expressions
        ("""
        delete x;
        typeof x;
        void x;
        x += (!y)++;
        """,
         'delete x;typeof x;void x;x+=(!y)++;'),

        # label + break label + continue label
        ("""
        label:
        if ( i == 0 )
          continue label;
        switch (day) {
        case 5:
          break ;
        default:
          break label;
        }
        """,
         'label:if(i==0)continue label;switch(day){case 5:break;default:break label;}'),

        # break + continue: no labels
        ("""
        while (i <= 7) {
          if ( i == 3 )
              continue;
          if ( i == 0 )
              break;
        }
        """,
         'while(i<=7){if(i==3)continue;if(i==0)break;}'),

        # regex + one line statements in if and if .. else
        ("""
        function a(x, y) {
         var re = /ab+c/;
         if (x == 1)
           return x + y;
         if (x == 3)
           return {x: 1};
         else
           return;
        }
        """,
         'function a(x,y){var re=/ab+c/;if(x==1)return x+y;if(x==3)return{x:1};else return;}'),

        # new
        ('return new jQuery.fn.init( selector, context, rootjQuery );',
         'return new jQuery.fn.init(selector,context,rootjQuery);'
         ),

        # no space after 'else' when the next token is (, {
        ("""
        if (true) {
          x = true;
          y = 3;
        } else {
          x = false
          y = 5
        }
        """,
         'if(true){x=true;y=3;}else{x=false;y=5;}'),

        ("""
        if (true) {
          x = true;
          y = 3;
        } else
          (x + ' qw').split(' ');
        """,
         "if(true){x=true;y=3;}else(x+' qw').split(' ');"),


        ##############################################################
        # Block braces removal
        ##############################################################

        # do while
        ('do { x += 1; } while(true);', 'do x+=1;while(true);'),
        # do while: multiple statements
        ('do { x += 1; y += 1;} while(true);', 'do{x+=1;y+=1;}while(true);'),

        # elision
        ('var a = [1, 2, 3, ,,,5];', 'var a=[1,2,3,,,,5];'),

        # with
        ("""
        with (obj) {
          a = b;
        }
        """,
         'with(obj)a=b;'),

        # with: multiple statements
        ("""
        with (obj) {
          a = b;
          c = d;
        }
        """,
         'with(obj){a=b;c=d;}'),

        # if else
        ("""
        if (true) {
          x = true;
        } else {
          x = false
        }
        """,
         'if(true)x=true;else x=false;'),

        # if: multiple statements
        ("""
        if (true) {
          x = true;
          y = false;
        } else {
          x = false;
          y = true;
        }
        """,
         'if(true){x=true;y=false;}else{x=false;y=true;}'),

        # try catch finally: one statement
        ("""
        try {
          throw "my_exception"; // generates an exception
        }
        catch (e) {
          // statements to handle any exceptions
          log(e); // pass exception object to error handler
        }
        finally {
          closefiles(); // always close the resource
        }
        """,
         'try{throw "my_exception";}catch(e){log(e);}finally{closefiles();}'
         ),

        # try catch finally: no statements
        ("""
        try {
        }
        catch (e) {
        }
        finally {
        }
        """,
         'try{}catch(e){}finally{}'
         ),

        # try catch finally: multiple statements
        ("""
        try {
          x = 3;
          y = 5;
        }
        catch (e) {
          log(e);
          log('e');
        }
        finally {
          z = 7;
          log('z');
        }
        """,
         "try{x=3;y=5;}catch(e){log(e);log('e');}finally{z=7;log('z');}"
         ),

        # tricky case with an 'if' nested in 'if .. else'
        # We need to preserve braces in the first 'if' otherwise
        # 'else' might get associated with nested 'if' instead
        ("""
        if ( obj ) {
                for ( n in obj ) {
                        if ( v === false) {
                                break;
                        }
                }
        } else {
                for ( ; i < l; ) {
                        if ( nv === false ) {
                                break;
                        }
                }
        }
        """,
         'if(obj){for(n in obj)if(v===false)break;}else for(;i<l;)if(nv===false)break;'),

        # We don't care about nested 'if' when enclosing 'if' block
        # contains multiple statements because braces won't be removed
        # by visit_Block when there are multiple statements in the block
        ("""
        if ( obj ) {
                for ( n in obj ) {
                        if ( v === false) {
                                break;
                        }
                }
                x = 5;
        } else {
                for ( ; i < l; ) {
                        if ( nv === false ) {
                                break;
                        }
                }
        }
        """,
         'if(obj){for(n in obj)if(v===false)break;x=5;}else for(;i<l;)if(nv===false)break;'),


        # No dangling 'else' - remove braces
        ("""
        if ( obj ) {
                for ( n in obj ) {
                        if ( v === false) {
                                break;
                        } else {
                                n = 3;
                        }
                }
        } else {
                for ( ; i < l; ) {
                        if ( nv === false ) {
                                break;
                        }
                }
        }
        """,
         'if(obj)for(n in obj)if(v===false)break;else n=3;else for(;i<l;)if(nv===false)break;'),

        # foo["bar"] --> foo.bar
        ('foo["bar"];', 'foo.bar;'),
        ("foo['bar'];", 'foo.bar;'),
        ("""foo['bar"']=42;""", """foo['bar"']=42;"""),
        ("""foo["bar'"]=42;""", """foo["bar'"]=42;"""),
        ('foo["bar bar"];', 'foo["bar bar"];'),
        ('foo["bar"+"bar"];', 'foo["bar"+"bar"];'),
        # https://github.com/rspivak/slimit/issues/34
        # test some reserved keywords
        ('foo["for"];', 'foo["for"];'),
        ('foo["class"];', 'foo["class"];'),


        # https://github.com/rspivak/slimit/issues/21
        # c||(c=393,a=323,b=2321); --> c||c=393,a=323,b=2321; ERROR
        ('c||(c=393);', 'c||(c=393);'),
        ('c||(c=393,a=323,b=2321);', 'c||(c=393,a=323,b=2321);'),

        # https://github.com/rspivak/slimit/issues/25
        ('for(a?b:c;d;)e=1;', 'for(a?b:c;d;)e=1;'),

        # https://github.com/rspivak/slimit/issues/26
        ('"begin"+ ++a+"end";', '"begin"+ ++a+"end";'),

        # https://github.com/rspivak/slimit/issues/28
        ("""
         (function($) {
             $.hello = 'world';
         }(jQuery));
         """,
         "(function($){$.hello='world';}(jQuery));"),

        # function call in FOR init
        ('for(o(); i < 3; i++) {}', 'for(o();i<3;i++){}'),

        # unary increment operator in FOR init
        ('for(i++; i < 3; i++) {}', 'for(i++;i<3;i++){}'),

        # unary decrement operator in FOR init
        ('for(i--; i < 3; i++) {}', 'for(i--;i<3;i++){}'),

        # issue-37, simple identifier in FOR init
        ('for(i; i < 3; i++) {}', 'for(i;i<3;i++){}'),

        # https://github.com/rspivak/slimit/issues/32
        ("""
         Name.prototype = {
           getPageProp: function Page_getPageProp(key) {
             return this.pageDict.get(key);
           },

           get fullName() {
             return this.first + " " + this.last;
           },

           set fullName(name) {
             var names = name.split(" ");
             this.first = names[0];
             this.last = names[1];
           }
         };
         """,
         ('Name.prototype={getPageProp:function Page_getPageProp(key){'
          'return this.pageDict.get(key);},'
          'get fullName(){return this.first+" "+this.last;},'
          'set fullName(name){var names=name.split(" ");this.first=names[0];'
          'this.last=names[1];}};')
        ),

        # https://github.com/rspivak/slimit/issues/47 - might be a Python 3
        # related issue
        ('testObj[":"] = undefined; // Breaks', 'testObj[":"]=undefined;'),
        ('testObj["::"] = undefined; // Breaks', 'testObj["::"]=undefined;'),
        ('testObj["a:"] = undefined; // Breaks', 'testObj["a:"]=undefined;'),
        ('testObj["."] = undefined; // OK', 'testObj["."]=undefined;'),
        ('testObj["{"] = undefined; // OK', 'testObj["{"]=undefined;'),
        ('testObj["}"] = undefined; // OK', 'testObj["}"]=undefined;'),
        ('testObj["["] = undefined; // Breaks', 'testObj["["]=undefined;'),
        ('testObj["]"] = undefined; // Breaks', 'testObj["]"]=undefined;'),
        ('testObj["("] = undefined; // OK', 'testObj["("]=undefined;'),
        ('testObj[")"] = undefined; // OK', 'testObj[")"]=undefined;'),
        ('testObj["="] = undefined; // Breaks', 'testObj["="]=undefined;'),
        ('testObj["-"] = undefined; // OK', 'testObj["-"]=undefined;'),
        ('testObj["+"] = undefined; // OK', 'testObj["+"]=undefined;'),
        ('testObj["*"] = undefined; // OK', 'testObj["*"]=undefined;'),
        ('testObj["/"] = undefined; // OK', 'testObj["/"]=undefined;'),
        (r'testObj["\\"] = undefined; // Breaks', r'testObj["\\"]=undefined;'),
        ('testObj["%"] = undefined; // OK', 'testObj["%"]=undefined;'),
        ('testObj["<"] = undefined; // Breaks', 'testObj["<"]=undefined;'),
        ('testObj[">"] = undefined; // Breaks', 'testObj[">"]=undefined;'),
        ('testObj["!"] = undefined; // OK', 'testObj["!"]=undefined;'),
        ('testObj["?"] = undefined; // Breaks', 'testObj["?"]=undefined;'),
        ('testObj[","] = undefined; // OK', 'testObj[","]=undefined;'),
        ('testObj["@"] = undefined; // Breaks', 'testObj["@"]=undefined;'),
        ('testObj["#"] = undefined; // OK', 'testObj["#"]=undefined;'),
        ('testObj["&"] = undefined; // OK', 'testObj["&"]=undefined;'),
        ('testObj["|"] = undefined; // OK', 'testObj["|"]=undefined;'),
        ('testObj["~"] = undefined; // OK', 'testObj["~"]=undefined;'),
        ('testObj["`"] = undefined; // Breaks', 'testObj["`"]=undefined;'),
        ('testObj["."] = undefined; // OK', 'testObj["."]=undefined;'),
        ]

