#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import division
from __future__ import absolute_import

import warnings
with warnings.catch_warnings():
    warnings.simplefilter('ignore', DeprecationWarning)

    from future import standard_library
    standard_library.install_aliases()

from builtins import map
from future.utils import string_types, native_str, integer_types, text_type, PY2

try:
    import cPickle as pickle
except ImportError:
    import pickle

from decimal import Decimal
import json
import operator
import os
import re
import string
import sys
from collections import namedtuple
import unittest

import fileseq
from fileseq import (FrameSet,
                     FileSequence,
                     findSequencesOnDisk,
                     findSequenceOnDisk,
                     padFrameRange,
                     getPaddingChars,
                     getPaddingNum,
                     FileSeqException)

from fileseq import constants, exceptions, utils
from fileseq.constants import PAD_MAP


TEST_DIR = os.path.abspath(os.path.dirname(__file__))
SRC_DIR = os.path.join(TEST_DIR, "../src")
sys.path.insert(0, SRC_DIR)
os.chdir(TEST_DIR)


# For testing compatibility with pickle values from older version of fileseq
PICKLE_TEST_SEQ = "/path/to/file.1-100x2#.exr"
OLD_PICKLE_MAP = {
    '1.10.0': b'\x80\x02cfileseq.filesequence\nFileSequence\nq\x01)\x81q\x02}q\x03(U\x04_extq\x04U\x04.exrU'
              b'\t_frameSetq\x05cfileseq.frameset\nFrameSet\nq\x06)\x81q\x07U\x071-100x2q\x08\x85bU\x04_dirq'
              b'\tU\t/path/to/U\x04_padq\nU\x01#U\x05_baseq\x0bU\x05file.U\x06_zfillq\x0cK\x04ub.'
}


def _getCommonPathSep(path):
    """
    Find the most common path seperator character used
    in a given path. Because windows supports both forward
    and backward sep characters, find the most consistently
    used.
    Defaults to ``os.sep``

    :type path: str
    :param path: A path to check for the most common sep
    :rtype: str
    """
    sep = os.sep
    count = 0
    for nextSep in ('/', '\\'):
        if path.count(nextSep) > count:
            sep = nextSep
    return sep


utils._getPathSep = _getCommonPathSep


class TestUtils(unittest.TestCase):

    def testXrangeOverflow(self):
        # Test that a platform-specific xrange does not produce OverflowError
        xrng = utils.xrange(1, sys.maxsize)
        self.assertTrue(len(xrng) != 0)

    def testXrange(self):
        table = [
            (1, 1, 1),
            (1, 20, -1),
            (1, 20, 1),
            (1, 20, 2),
            (1, 20, 3),
            (1, 21, 1),
            (1, 21, 2),
            (1, 21, 3),
            (20, 1, 1),
            (20, 1, -1),
            (20, 1, -2),
            (20, 1, -3),
            (21, 1, 1),
            (21, 1, -1),
            (21, 1, -2),
            (21, 1, -3)
        ]
        for start, stop, step in table:
            expected = list(range(start, stop, step))
            actual = utils.xrange2(start, stop, step)
            self.assertEqual(len(expected),  utils.lenRange(start, stop, step))
            self.assertEqual(len(expected), len(actual))
            self.assertEqual(expected, list(actual))

    def testXfrange(self):
        Case = namedtuple('Case', ['start', 'stop', 'step', 'len'])
        table = [
            Case(1, 1, 1, 1),
            Case(1, 20, -1, 20),
            Case(1, 20, 1, 20),
            Case(1, 20, 2, 10),
            Case(1, 20, 3, 7),
            Case(1, 21, 1, 21),
            Case(1, 21, 2, 11),
            Case(1, 21, 3, 7),
            Case(20, 1, 1, 20),
            Case(20, 1, -1, 20),
            Case(20, 1, -2, 10),
            Case(20, 1, -3, 7),
            Case(21, 1, 1, 21),
            Case(21, 1, -1, 21),
            Case(21, 1, -2, 11),
            Case(21, 1, -3, 7)
        ]
        for case in table:
            actual = utils.xfrange(case.start, case.stop, case.step)
            self.assertIsInstance(actual, utils._xfrange)
            self.assertEqual(case.start,  actual.start, msg=str(case))
            self.assertEqual(case.stop,  actual.stop, msg=str(case))
            if case.start <= case.stop:
                self.assertEqual(abs(case.step),  actual.step, msg=str(case))
            else:
                self.assertEqual(-(abs(case.step)),  actual.step, msg=str(case))
            self.assertEqual(case.len, len(actual), msg=str(case))

    def testAsString(self):
        expect = "my string"
        custom = _CustomPathString(expect)
        self.assertEqual(expect, custom)

        # https://github.com/justinfx/fileseq/issues/106
        actual = utils.asString(custom)
        self.assertEqual(expect, actual)
        self.assertIsInstance(actual, str)
        self.assertNotIsInstance(actual, _CustomPathString)

    def testBatchFrames(self):
        Case = namedtuple('Case', ['start', 'stop', 'batch_size', 'expect'])
        table = [
            Case(1,  5, -1, []),
            Case(1,  5,  0, []),
            Case(1,  5,  1, [[1], [2], [3], [4], [5]]),
            Case(1,  5,  2, [[1,2], [3,4], [5]]),
            Case(1,  5,  3, [[1,2,3], [4,5]]),
            Case(1,  5,  4, [[1,2,3,4], [5]]),
            Case(1,  5,  5, [[1,2,3,4,5]]),
            Case(1,  5,  9, [[1,2,3,4,5]]),
            Case(2, 13,  3, [[2,3,4], [5,6,7], [8,9,10], [11,12,13]]),
            Case(2, 13,  4, [[2,3,4,5], [6,7,8,9], [10,11,12,13]]),

            Case( 5, 1, 1, [[5], [4], [3], [2], [1]]),
            Case(13, 2, 2, [[13,12], [11,10], [9,8], [7,6], [5,4], [3,2]]),
            Case(13, 2, 3, [[13,12,11], [10,9,8], [7,6,5], [4,3,2]]),
            Case(13, 2, 4, [[13,12,11,10], [9,8,7,6], [5,4,3,2]]),
            Case(13, 2, 5, [[13,12,11,10,9], [8,7,6,5,4], [3,2]]),
            Case(13, 2, 6, [[13,12,11,10,9,8], [7,6,5,4,3,2]]),
            Case(13, 1, 6, [[13,12,11,10,9,8], [7,6,5,4,3,2], [1]]),
        ]

        for case in table:
            ret = utils.batchFrames(case.start, case.stop, case.batch_size)
            batches = [i for i in ret]
            actual = [list(i) for i in batches]
            self.assertEqual(case.expect, actual, msg=str(case))

            expect_len = sum(len(i) for i in case.expect)
            actual_len = sum(len(i) for i in batches)
            self.assertEqual(expect_len, actual_len, msg=str(case))

            for i, actual_sub in enumerate(batches):
                expect_sub = case.expect[i]
                msg = '{!s} sub={!r}'.format(case, expect_sub)
                self.assertEqual(expect_sub[0], actual_sub.start, msg=msg)
                self.assertEqual(expect_sub[-1], actual_sub.stop, msg=msg)
                expect_step = 1 if actual_sub.start <= actual_sub.stop else -1
                self.assertEqual(expect_step, actual_sub.step, msg=msg)
                self.assertEqual(len(expect_sub), len(actual_sub), msg=msg)

    def testBatchIterable(self):
        Case = namedtuple('Case', ['it', 'batches', 'expect'])
        _range = range  # prevent pylint warning W1638
        table = [
            Case(['a', 'b', 'c'],   0, []),
            Case(['a', 'b', 'c'],  -1, []),
            Case(['a', 'b', 'c'],   1, [['a'], ['b'], ['c']]),
            Case(['a', 'b', 'c'],   2, [['a', 'b'], ['c']]),
            Case(['a', 'b', 'c'],   3, [['a', 'b', 'c']]),
            Case(['a', 'b', 'c'],   9, [['a', 'b', 'c']]),

            Case('abc',  0, []),
            Case('abc',  1, [['a'], ['b'], ['c']]),
            Case('abc',  2, [['a', 'b'], ['c']]),
            Case('abc',  3, [['a', 'b', 'c']]),
            Case('abc',  9, [['a', 'b', 'c']]),

            Case(_range(1,5),  0, []),
            Case(_range(1,5),  1, [[1], [2], [3], [4]]),
            Case(_range(1,5),  2, [[1,2], [3,4]]),
            Case(_range(1,5),  3, [[1,2,3], [4]]),
            Case(_range(1,5),  4, [[1,2,3,4]]),
            Case(_range(1,5),  5, [[1,2,3,4]]),
            Case(_range(1,5),  9, [[1,2,3,4]]),
        ]

        def gen(): return (i for i in 'abc')
        table += [
            Case(gen(),  0, []),
            Case(gen(),  1, [['a'], ['b'], ['c']]),
            Case(gen(),  2, [['a', 'b'], ['c']]),
            Case(gen(),  3, [['a', 'b', 'c']]),
            Case(gen(),  4, [['a', 'b', 'c']]),
            Case(gen(),  9, [['a', 'b', 'c']]),
        ]

        def _iter(): return iter([1,2,3,4,5,6,7])
        table += [
            Case(_iter(),  0, []),
            Case(_iter(),  1, [[1], [2], [3], [4], [5], [6], [7]]),
            Case(_iter(),  2, [[1,2], [3,4], [5,6], [7]]),
            Case(_iter(),  3, [[1,2,3], [4,5,6], [7]]),
            Case(_iter(),  4, [[1,2,3,4], [5,6,7]]),
            Case(_iter(),  5, [[1,2,3,4,5], [6,7]]),
            Case(_iter(),  6, [[1,2,3,4,5,6], [7]]),
            Case(_iter(),  7, [[1,2,3,4,5,6,7]]),
            Case(_iter(),  9, [[1,2,3,4,5,6,7]]),
        ]

        for case in table:
            actual = utils.batchIterable(case.it, case.batches)
            actual = [list(i) for i in actual]
            self.assertEqual(case.expect, actual, msg=str(case))


class TestFrameSet(unittest.TestCase):

    def testFrameValues(self):
        class Case:
            def __init__(self, src, expected=None, has_subframes=False, err=None):
                self.src = src
                self.expected = expected
                self.has_subframes = has_subframes
                self.err = err

        table = [
            Case(
                [Decimal('1'), Decimal('5.8'), Decimal('10')],
                [Decimal('1'), Decimal('5.8'), Decimal('10')],
                True
            ),
            Case(
                [1, Decimal('5.8'), 10],
                [Decimal('1'), Decimal('5.8'), Decimal('10')],
                True
            ),
            Case(
                ['1', '5.8', '10'],
                [Decimal('1'), Decimal('5.8'), Decimal('10')],
                True
            ),
            Case(
                [1, '5', Decimal('10')],
                [1, 5, 10],
            ),
            Case(
                [1, '5', '10'],
                [1, 5, 10],
            ),
            Case(
                [Decimal('1.5'), Decimal('5'), Decimal('10.2')],
                [Decimal('1.5'), Decimal('5'), Decimal('10.2')],
                True
            ),
            Case(
                [Decimal('1.001'), Decimal('5'), Decimal('10.999')],
                [Decimal('1.001'), Decimal('5'), Decimal('10.999')],
                True
            ),
            Case(
                [Decimal('-0.25'), Decimal('0'), Decimal('0.25')],
                [Decimal('-0.25'), Decimal('0'), Decimal('0.25')],
                True
            ),
            Case(
                [Decimal('1'), Decimal('2'), Decimal('3')],
                [1, 2, 3],
            ),
            Case(
                [Decimal('1.0'), Decimal('2'), Decimal('3')],
                [1, 2, 3],
            ),
            Case(
                ['1.0', 2, 3],
                [1, 2, 3],
            ),
            Case(
                [1.5, 2.5, 3.5],
                [1.5, 2.5, 3.5],
                True
            ),
            Case(
                [1.0, 2.0, 3.0],
                [1, 2, 3],
            ),
            Case(
                [1, 5.8, 10],
                [1.0, 5.8, 10.0],
                True
            ),
            Case(
                [1.5, 5, 10.2],
                [1.5, 5.0, 10.2],
                True
            ),
            Case(
                [1.001, 5, 10.999],
                [1.001, 5.0, 10.999],
                True
            ),
            Case(
                [0, '0.3333', '0.6667', 1, '1.3333', '1.6667'],
                [Decimal('0.0000'), Decimal('0.3333'), Decimal('0.6667'),
                 Decimal('1.0000'), Decimal('1.3333'), Decimal('1.6667')],
                True
            ),
            Case(
                [0, '0.3333', '0.6667', 1, '1.3333', '1.6667', 2],
                [Decimal('0.0000'), Decimal('0.3333'), Decimal('0.6667'),
                 Decimal('1.0000'), Decimal('1.3333'), Decimal('1.6667'),
                 Decimal('2.0000')],
                True
            ),
            Case(
                [0, '0.1429', '0.2857', '0.4286', '0.5714', '0.7143', '0.8571',
                 1, '1.1429', '1.2857', '1.4286', '1.5714', '1.7143', '1.8571'],
                [Decimal('0.0000'), Decimal('0.1429'), Decimal('0.2857'),
                 Decimal('0.4286'), Decimal('0.5714'), Decimal('0.7143'),
                 Decimal('0.8571'), Decimal('1.0000'), Decimal('1.1429'),
                 Decimal('1.2857'), Decimal('1.4286'), Decimal('1.5714'),
                 Decimal('1.7143'), Decimal('1.8571')],
                True
            ),
            Case(
                [0, '0.1429', '0.2857', '0.4286', '0.5714', '0.7143', '0.8571',
                 1, '1.1429', '1.2857', '1.4286', '1.5714', '1.7143', '1.8571',
                 2],
                [Decimal('0.0000'), Decimal('0.1429'), Decimal('0.2857'),
                 Decimal('0.4286'), Decimal('0.5714'), Decimal('0.7143'),
                 Decimal('0.8571'), Decimal('1.0000'), Decimal('1.1429'),
                 Decimal('1.2857'), Decimal('1.4286'), Decimal('1.5714'),
                 Decimal('1.7143'), Decimal('1.8571'), Decimal('2.0000')],
                True
            ),
            Case(
                [1, '1.1429', '1.2857', '1.4286', '1.5714', '1.7143', '1.8571',
                 2],
                [Decimal('1.0000'), Decimal('1.1429'), Decimal('1.2857'),
                 Decimal('1.4286'), Decimal('1.5714'), Decimal('1.7143'),
                 Decimal('1.8571'), Decimal('2.0000')],
                True
            ),
            Case(
                [utils.quantize(Decimal(x) / Decimal(3), 5) for x in range(301)],
                [utils.quantize(Decimal(x) / Decimal(3), 5) for x in range(301)],
                True
            ),
            Case(
                [utils.quantize(Decimal(x) / Decimal(14), 5) for x in range(701)],
                [utils.quantize(Decimal(x) / Decimal(14), 5) for x in range(701)],
                True
            ),
            Case('abc', None, err=fileseq.ParseException),
            Case('a-z', None, err=fileseq.ParseException),
            Case(['a', 'b'], None, err=fileseq.ParseException),
            Case([1, 'b'], None, err=fileseq.ParseException),
        ]

        neg_table = []
        for case in table:
            if case.err is not None:
                continue
            neg_src = []
            for x in case.src:
                if isinstance(x, integer_types + (float, Decimal)):
                    neg_src.append(-x)
                elif x.startswith('-'):
                    neg_src.append(x[1:])
                else:
                    neg_src.append('-' + x.lstrip('+'))
            neg_expected = list(map(operator.neg, case.expected))
            neg_table.append(Case(neg_src, neg_expected, case.has_subframes))

        for case in table + neg_table:
            expected = case.expected
            if case.err is not None:
                self.assertRaises(case.err, FrameSet, case.src)
                continue
            f = FrameSet(case.src)
            actual = list(f)
            self.assertEqual(actual, expected)
            self.assertEqual(case.has_subframes, f.hasSubFrames())

            actual = list(FrameSet(f.frange))
            # floats will be converted to decimal during roundtrip to frange
            if any(isinstance(x, float) for x in expected):
                expected = [Decimal(str(x)) for x in expected]
            # decimals will need to be rounded to match exactly
            if any(isinstance(x, Decimal) for x in expected):
                for i, (e, a) in enumerate(zip(expected, actual)):
                    actual[i] = a.quantize(e)
            self.assertEqual(actual, expected)
            self.assertEqual(case.has_subframes, f.hasSubFrames())

    def testMaxFrameSize(self):
        _maxSize = constants.MAX_FRAME_SIZE
        try:
            maxSize = constants.MAX_FRAME_SIZE = 500

            # Within range
            utils.xfrange(1, 100, 1, maxSize=-1)
            utils.xfrange(1, 100, 1, maxSize=100)
            FrameSet('1-%d' % maxSize)

            # Should not be allowed
            self.assertRaises(exceptions.MaxSizeException, utils.xfrange, 1, 100, 1, maxSize=50)
            self.assertRaises(exceptions.MaxSizeException, FrameSet, '1-%d' % (maxSize + 1))

            # Inverting would produce a huge new range
            fs = FrameSet('1,%d' % (maxSize + 3))
            self.assertRaises(exceptions.MaxSizeException, fs.invertedFrameRange)

        finally:
            constants.MAX_FRAME_SIZE = _maxSize

    def test2FramesContiguous(self):
        table = [
            ([1, 2], "1-2"),
            ([-1, 0], "-1-0"),
            ([-2, -1], "-2--1"),
            ([1, 2, 5, 7, 8, 10, 11], "1-2,5,7-8,10-11"),
            ([-5, -4, -1, 1, 2, 5, 7, 8, 12, 13, 14, 15, 16, 52, 53], "-5--4,-1,1-2,5,7-8,12-16,52-53"),
        ]

        for frames, expected in table:
            fs = FrameSet(frames)
            self.assertEqual(str(fs), expected)

    def testIsConsecutive(self):
        consec = [
            "10-100",
            "-100-100",
            "-100--50",
            "1,2,3,4,5",
            "5,6,7,8,9",
            "-5,-4,-3,-2,-1,0,1",
            "1,2,3,4,3,2,3,4,5,4",
            "-10-3,1-10,5-20,7-30",
            "10--10",
            "10-2,8-0,4--5",
        ]

        nonconsec = [
            "10-20x2",
            "10-20x2,15-30x3",
            "-5-1,3-10",
            "-10-10x2",
            "10--1010x2",
            "1,2,3,4,6,7,8",
            "1,2,3,4,5,0",
            "5,4,3,2,1,6",
            "1.1,1.2"
        ]

        for t in consec:
            self.assertTrue(FrameSet(t).isConsecutive(),
                            "Expected %s to be consecutive" % t)

        for t in nonconsec:
            self.assertFalse(FrameSet(t).isConsecutive(),
                             "Expected %s to not be consecutive" % t)

    def testSlicing(self):
        Case = namedtuple('Case', ['input', 'slice', 'expected'])
        table = [
            Case('1-10', slice(3, 6), (4, 5, 6)),
            Case('1-10', slice(None, 5), (1, 2, 3, 4, 5)),
            Case('1-10', slice(5, None), (6, 7, 8, 9, 10)),
            Case('1-10', slice(-3, None), (8, 9, 10)),
            Case('1-10', slice(-6, None, 2), (5, 7, 9)),
            Case('1-10x0.5', slice(1,3), (Decimal('1.5'), Decimal('2.0'))),
            Case('1-10x0.5', slice(-3, None), (Decimal('9.0'), Decimal('9.5'), Decimal('10.0')))
        ]

        for case in table:
            fs = FrameSet(case.input)
            actual = fs[case.slice]
            self.assertEqual(case.expected, actual)

    def testSubFrames(self):
        table = [
            ("1-2x0.25",
             [Decimal("1.00"), Decimal("1.25"), Decimal("1.50"), Decimal("1.75"),
             Decimal("2.00")]
            ),
            ("1,1.5",
             [Decimal("1.0"), Decimal("1.5")]
             ),
            ("1-2,3-4x0.5",
             [Decimal("1.0"), Decimal("2.0"), Decimal("3.0"), Decimal("3.5"),
              Decimal("4.0")]
            ),
            ("1-2x0.3",
             [Decimal("1.0"), Decimal("1.3"), Decimal("1.6"), Decimal("1.9")]
            ),

            ("1.5,1",
             [Decimal("1.5"), Decimal("1.0")]
            ),
            ("4-3x0.5,2-1",
             [Decimal("4.0"), Decimal("3.5"), Decimal("3.0"), Decimal("2.0"),
              Decimal("1.0")]
            ),
            ("2-1x0.3",
             [Decimal("2.0"), Decimal("1.7"), Decimal("1.4"), Decimal("1.1")]
            ),

        ]

        for frames, expected in table:
            fs = FrameSet(frames)
            actual = list(fs)
            self.assertEqual(expected, actual)

    def testStrUnicode(self):
        """https://github.com/justinfx/fileseq/issues/99"""
        ret = FrameSet(u'1-10')
        _ = str(ret)
        _ = text_type(ret)
        val = repr(ret)

    def testBatchesFrames(self):
        D = Decimal
        Case = namedtuple('Case', ['input', 'batch', 'expect'])
        table = [
            Case("1-3", 0, []),
            Case("1-3", 1, [[1], [2], [3]]),
            Case("1-3", 2, [[1,2], [3]]),
            Case("1-3", 3, [[1,2,3]]),
            Case("1-3", 9, [[1,2,3]]),

            Case("6-7x0.2", 1, [[D('6.0')], [D('6.2')], [D('6.4')], [D('6.6')], [D('6.8')], [D('7.0')]]),
            Case("6-7x0.2", 3, [[D('6.0'),D('6.2'),D('6.4')], [D('6.6'),D('6.8'),D('7.0')]]),
        ]

        for case in table:
            fs = FrameSet(case.input)
            actual = [list(i) for i in fs.batches(case.batch, frames=True)]
            self.assertEqual(case.expect, actual, msg=str(case))

    def testBatches(self):
        D = Decimal
        Case = namedtuple('Case', ['input', 'batch', 'expect'])
        table = [
            Case("1-3", 0, []),
            Case("1-3", 1, ['1', '2', '3']),
            Case("1-3", 2, ['1-2', '3']),
            Case("1-3", 3, ['1-3']),
            Case("1-3", 9, ['1-3']),

            Case("6-7x0.2", 1, ['6', '6.2', '6.4', '6.6', '6.8', '7']),
            Case("6-7x0.2", 3, ['6-6.4x0.2', '6.6-7x0.2']),
        ]

        for case in table:
            expect = [FrameSet(i) for i in case.expect]
            actual = list(FrameSet(case.input).batches(case.batch, frames=False))
            self.assertListEqual(expect, actual, msg=str(case))


class TestBase(unittest.TestCase):
    RX_PATHSEP = re.compile(r'[/\\]')

    def assertEquals(self, a, b, msg=None):
        # Make sure string paths are compared with normalized
        # path separators
        if isinstance(a, string_types) and isinstance(b, string_types):
            a = os.path.normpath(a)
            b = os.path.normpath(b)

        super(TestBase, self).assertEqual(a, b, msg=msg)

    def assertEqual(self, a, b, msg=None):
        self.assertEquals(a, b, msg=msg)

    def assertEqualPaths(self, a, b, msg=None):
        return super(TestBase, self).assertEqual(
            self.toNormpaths(a), self.toNormpaths(b),
            msg=msg)

    def assertNativeStr(self, a):
        self.assertIsInstance(a, native_str, '{0!r} != {1!r}'.format(a, native_str))

    def toNormpaths(self, collection):
        if isinstance(collection, string_types):
            collection = [collection]
        return sorted(os.path.normpath(p) for p in collection)


class _CustomPathString(str):
    """
    Custom string will always treat substring slices
    as path components, and normalize them by removing
    trailing and duplicate path seps
    """

    @classmethod
    def _create(cls, val):
        if val:
            val = os.path.normpath(str(val))
        return cls(val)

    def __new__(cls, path):
        if path:
            path = os.path.normpath(path)
        return super(_CustomPathString, cls).__new__(cls, path)

    def __add__(self, other):
        return self._create(super(_CustomPathString, self).__add__(other))

    def __getitem__(self, item):
        return self._create(super(_CustomPathString, self).__getitem__(item))


class TestFileSequence(TestBase):

    def testNativeStr(self):
        seq = FileSequence("/foo/boo.1-5#.exr")
        self.assertNativeStr(seq.dirname())
        self.assertNativeStr(seq.basename())
        self.assertNativeStr(seq.padding())
        self.assertNativeStr(seq.extension())
        self.assertNativeStr(seq.extension())
        self.assertNativeStr(seq.format('{basename}'))
        self.assertNativeStr(seq.frame(1))
        self.assertNativeStr(seq.frameRange())
        self.assertNativeStr(seq.index(1))
        self.assertNativeStr(seq.invertedFrameRange())

        self.assertNativeStr(FileSequence.conformPadding('#'))
        self.assertNativeStr(FileSequence.getPaddingChars(4))

    def testSeqGettersType1(self):
        seq = FileSequence("/foo/boo.1-5#.exr")
        self.assertEquals(5, len(seq))
        self.assertEquals("/foo/", seq.dirname())
        self.assertNativeStr(seq.dirname())
        self.assertEquals("boo.", seq.basename())
        self.assertNativeStr(seq.basename())
        self.assertEquals("#", seq.padding())
        self.assertNativeStr(seq.padding())
        self.assertEquals(".exr", seq.extension())
        self.assertNativeStr(seq.extension())

        self.assertEquals("/foo/boo.9999.exr", seq.frame(9999))
        self.assertNativeStr(seq.frame(9999))
        self.assertEquals("/foo/boo.0001.exr", seq[0])
        self.assertNativeStr(seq[0])
        self.assertEquals("/foo/boo.0001.exr", seq.index(0))
        self.assertNativeStr(seq.index(0))

    def testSeqGettersType2(self):
        seq = FileSequence("/foo/boo1-5#.exr")
        self.assertEquals(5, len(seq))
        self.assertEquals("/foo/", seq.dirname())
        self.assertEquals("boo", seq.basename())
        self.assertEquals("#", seq.padding())
        self.assertEquals(".exr", seq.extension())

        self.assertEquals("/foo/boo9999.exr", seq.frame(9999))
        self.assertEquals("/foo/boo0001.exr", seq[0])
        self.assertEquals("/foo/boo0001.exr", seq.index(0))

    def testSeqGettersPrintf(self):
        seq = FileSequence("/foo/boo.1-5%04d.exr")
        self.assertEquals(5, len(seq))
        self.assertEquals("/foo/", seq.dirname())
        self.assertEquals("boo.", seq.basename())
        self.assertEquals("%04d", seq.padding())
        self.assertEquals(".exr", seq.extension())

        self.assertEquals("/foo/boo.9999.exr", seq.frame(9999))
        self.assertEquals("/foo/boo.0001.exr", seq[0])
        self.assertEquals("/foo/boo.0001.exr", seq.index(0))

    def testSeqGettersHoudini(self):
        seq = FileSequence("/foo/boo.1-5$F.exr")
        self.assertEquals(5, len(seq))
        self.assertEquals("/foo/", seq.dirname())
        self.assertEquals("boo.", seq.basename())
        self.assertEquals("$F", seq.padding())
        self.assertEquals(".exr", seq.extension())

        self.assertEquals("/foo/boo.9999.exr", seq.frame(9999))
        self.assertEquals("/foo/boo.1.exr", seq[0])
        self.assertEquals("/foo/boo.1.exr", seq.index(0))

    def testSeqGettersHoudiniPadded(self):
        seq = FileSequence("/foo/boo.1-5$F4.exr")
        self.assertEquals(5, len(seq))
        self.assertEquals("/foo/", seq.dirname())
        self.assertEquals("boo.", seq.basename())
        self.assertEquals("$F4", seq.padding())
        self.assertEquals(".exr", seq.extension())

        self.assertEquals("/foo/boo.9999.exr", seq.frame(9999))
        self.assertEquals("/foo/boo.0001.exr", seq[0])
        self.assertEquals("/foo/boo.0001.exr", seq.index(0))

    def testSeqGettersUdim(self):
        seq = FileSequence("/foo/boo.1-5<UDIM>.exr")
        self.assertEquals(5, len(seq))
        self.assertEquals("/foo/", seq.dirname())
        self.assertEquals("boo.", seq.basename())
        self.assertEquals("<UDIM>", seq.padding())
        self.assertEquals(".exr", seq.extension())

        self.assertEquals("/foo/boo.9999.exr", seq.frame(9999))
        self.assertEquals("/foo/boo.0001.exr", seq[0])
        self.assertEquals("/foo/boo.0001.exr", seq.index(0))

    def testSeqGettersUdimD(self):
        seq = FileSequence("/foo/boo.1-5%(UDIM)d.exr")
        self.assertEquals(5, len(seq))
        self.assertEquals("/foo/", seq.dirname())
        self.assertEquals("boo.", seq.basename())
        self.assertEquals("%(UDIM)d", seq.padding())
        self.assertEquals(".exr", seq.extension())

        self.assertEquals("/foo/boo.9999.exr", seq.frame(9999))
        self.assertEquals("/foo/boo.0001.exr", seq[0])
        self.assertEquals("/foo/boo.0001.exr", seq.index(0))

    def testSetDirname(self):
        seq = FileSequence("/foo/bong.1-5@.exr")
        seq.setDirname("/bing/")
        self.assertEquals("/bing/bong.1.exr", seq[0])

        seq = FileSequence("/foo/bong.1-5@.exr")
        seq.setDirname("bing")
        self.assertEquals("bing/bong.1.exr", seq[0])

    def testSetBasename(self):
        seq = FileSequence("/foo/bong.1-5@.exr")
        seq.setBasename("bar.")
        self.assertEquals("/foo/bar.1.exr", seq[0])

    def testSetPadding(self):
        seq = FileSequence("/foo/bong.1-5@.exr")
        seq.setPadding("#")
        self.assertEquals("/foo/bong.0001.exr", seq[0])

        seq.setPadding("%02d")
        self.assertEquals("/foo/bong.01.exr", seq[0])

        expect = "/foo/bong.1-5#.exr"
        seq = FileSequence(expect)
        with self.assertRaises(ValueError):
            seq.setPadding("bad")
        self.assertEquals(expect, str(seq))

        expect = "/foo/bong.1-5#.10-20@@.exr"
        seq = FileSequence(expect)
        with self.assertRaises(ValueError):
            seq.setFramePadding("bad")
        self.assertEquals(expect, str(seq))

        expect = "/foo/bong.1-5#.10-20@@.exr"
        seq = FileSequence(expect)
        with self.assertRaises(ValueError):
            seq.setSubframePadding("bad")
        self.assertEquals(expect, str(seq))

    def testSetPadStyle(self):
        Case = namedtuple('Case', ['input', 'to_style', 'expected_pad'])
        H1 = fileseq.PAD_STYLE_HASH1
        H4 = fileseq.PAD_STYLE_HASH4
        FS = FileSequence

        table = [
            Case(FS('foo.1@.exr', H4), H1, '#'),
            Case(FS('foo.1@@@@.exr', H4), H1, '####'),
            Case(FS('foo.1#.exr', H4), H1, '####'),
            Case(FS('foo.1###@.exr', H4), H1, '#############'),
            Case(FS('foo.1-5x0.25#.####.exr', H4, allow_subframes=True), H1, '####.################'),
            Case(FS('foo.1-5x0.25###@.#.exr', H4, allow_subframes=True), H1, '#############.####'),

            Case(FS('foo.1@.exr', H1), H4, '@'),
            Case(FS('foo.1@@@@.exr', H1), H4, '#'),
            Case(FS('foo.1#.exr', H1), H4, '@'),
            Case(FS('foo.1####.exr', H1), H4, '#'),
            Case(FS('foo.1-5x0.25#.####.exr', H1, allow_subframes=True), H4, '@.#'),
            Case(FS('foo.1-5x0.25####.#.exr', H1, allow_subframes=True), H4, '#.@'),
        ]

        for case in table:
            actual = case.input.copy()
            actual.setPadStyle(case.to_style)
            self.assertEqual(case.input.zfill(), actual.zfill(), msg=str(case))
            self.assertEqual(case.input.decimalPlaces(), actual.decimalPlaces(), msg=str(case))
            self.assertEqual(case.expected_pad, actual.padding(), msg=str(case))

    def testSetPadStyleSetZfill(self):
        Case = namedtuple('Case', ['input', 'start_zfill', 'to_style', 'expected_zfill'])
        H1 = fileseq.PAD_STYLE_HASH1
        H4 = fileseq.PAD_STYLE_HASH4
        FS = FileSequence

        table = [
            Case(FS('foo.1@.exr', H4), 1, H1, 1),
            Case(FS('foo.1@@@@.exr', H4), 4, H1, 4),
            Case(FS('foo.1#.exr', H4), 4, H1, 1),
            Case(FS('foo.1####.exr', H4), 16, H1, 4),
            Case(FS('foo.1-5x0.25#.####.exr', H4, allow_subframes=True), 4, H1, 1),
            Case(FS('foo.1-5x0.25####.#.exr', H4, allow_subframes=True), 16, H1, 4),

            Case(FS('foo.1@.exr', H1), 1, H4, 1),
            Case(FS('foo.1@@@@.exr', H1), 4, H4, 4),
            Case(FS('foo.1#.exr', H1), 1, H4, 4),
            Case(FS('foo.1####.exr', H1), 4, H4, 16),
            Case(FS('foo.1-5x0.25#.####.exr', H1, allow_subframes=True), 1, H4, 4),
            Case(FS('foo.1-5x0.25####.#.exr', H1, allow_subframes=True), 4, H4, 16),
        ]

        for case in table:
            self.assertEqual(case.start_zfill, case.input.zfill(), msg=str(case))
            actual = case.input.copy()
            actual.setPadStyle(case.to_style, set_zfill=True)
            self.assertEqual(case.input.padding(), actual.padding(), msg=str(case))
            self.assertEqual(case.expected_zfill, actual.zfill(), msg=str(case))

    def testSetFrameSet(self):
        seq = FileSequence("/cheech/chong.1-5#.exr")
        seq.setFrameSet(FrameSet("10-20"))
        self.assertEquals("/cheech/chong.10-20#.exr", str(seq))

    def testSetFrameRange(self):
        seq = FileSequence("/cheech/chong.1-5#.exr")
        seq.setFrameRange("10-20")
        self.assertEquals("/cheech/chong.10-20#.exr", str(seq))

    def testFrame(self):
        seq = FileSequence("/foo/bar/bing.#.exr")
        self.assertEquals("/foo/bar/bing.0001.exr", seq.frame(1))
        self.assertNativeStr(seq.frame(1))
        self.assertEquals("/foo/bar/bing.#.exr", seq.frame("#"))
        self.assertNativeStr(seq.frame("#"))

        seq = FileSequence("/foo/bar/bing.%04d.exr")
        self.assertEquals("/foo/bar/bing.0001.exr", seq.frame(1))
        self.assertEquals("/foo/bar/bing.%04d.exr", seq.frame("%04d"))

    def testIter(self):
        known = {
            "/cheech/chong.0001.exr",
            "/cheech/chong.0003.exr",
            "/cheech/chong.0005.exr"
        }
        seq = FileSequence("/cheech/chong.1,3,5#.exr")
        self.assertFalse(known.difference(seq))

    def testSlicing(self):
        Case = namedtuple('Case', ['input', 'slice', 'expected'])
        table = [
            Case('file.1-10#.ext', 1, 'file.0002.ext'),
            Case('file.1-10#.ext', -1, 'file.0010.ext'),
            Case('file.1-10#.ext', slice(3, 6), FileSequence('file.4-6#.ext')),
            Case('file.1-10#.ext', slice(None, 5), FileSequence('file.1-5#.ext')),
            Case('file.1-10#.ext', slice(5, None), FileSequence('file.6-10#.ext')),
            Case('file.1-10#.ext', slice(-3, None), FileSequence('file.8-10#.ext')),
            Case('file.1-10#.ext', slice(-6, None, 2), FileSequence('file.5-9x2#.ext')),
            Case('file.-5-30x3#.ext', slice(3, 8, 2), FileSequence('file.4-16x6#.ext')),
            Case('file.-5-30x3#.ext', slice(None, None, 2), FileSequence('file.-5-25x6#.ext')),
        ]

        for case in table:
            fs = FileSequence(case.input)
            actual = fs[case.slice]
            self.assertEqual(case.expected, actual)

            if isinstance(actual, string_types):
                self.assertNativeStr(actual)

        fs = FileSequence('file.1-10#.ext')
        raises = [-20, 20, slice(11, None), slice(-200, -100)]
        for case in raises:
            with self.assertRaises(IndexError):
                ret = fs.__getitem__(case)
                raise RuntimeError("expected case %s to raise an IndexError; got %s" % (case, ret))

    def testFormat(self):
        seq = FileSequence("/cheech/chong.1-10,30,40#.exr")
        self.assertEquals("chong.0001-0010,0030,0040#.exr", str(seq.format()))
        self.assertNativeStr(seq.format())
        self.assertEquals("0011-0029,0031-0039", seq.format("{inverted}"))
        self.assertNativeStr(seq.format("{inverted}"))

        seq = findSequencesOnDisk("broken_seq")[0]
        self.assertEquals("0000-0002,0004,0006-0008", seq.format("{range}"))
        self.assertEquals("broke.0000-0002,0004,0006-0008#.exr", seq.format())
        seq = findSequencesOnDisk("step_seq")[0]
        self.assertEquals("step_seq/step1.1-13x4,14-17#.exr", str(seq))

    def testFormatInverted(self):
        _maxSize = constants.MAX_FRAME_SIZE
        try:
            maxSize = constants.MAX_FRAME_SIZE = 500

            # Test catching error for large inverted range
            seq = FileSequence("/path/to/file.1,%d#.ext" % (constants.MAX_FRAME_SIZE + 3))
            self.assertRaises(exceptions.MaxSizeException, seq.format, '{inverted}')

        finally:
            constants.MAX_FRAME_SIZE = _maxSize

    def testSplit(self):
        seqs = FileSequence("/cheech/chong.1-10,30,40#.exr").split()
        self.assertEquals("/cheech/chong.0001-0010#.exr", str(seqs[0]))
        self.assertEquals("/cheech/chong.0030#.exr", str(seqs[1]))
        self.assertEquals("/cheech/chong.0040#.exr", str(seqs[2]))

    def testMissingPeriods(self):
        seqs = FileSequence("/path/to/something_1-10#_exr")
        self.assertEquals("/path/to/something_0001_exr", seqs.index(0))

        seqs = FileSequence("/path/to/something_1-10%04d_exr")
        self.assertEquals("/path/to/something_0001_exr", seqs.index(0))

    def testNumericFilename(self):
        seqs = FileSequence("/path/to/1-10#.exr")
        self.assertEquals("/path/to/0001.exr", seqs.index(0))

        seqs = FileSequence("/path/to/1-10%04d.exr")
        self.assertEquals("/path/to/0001.exr", seqs.index(0))

    def testNoPlaceholder(self):
        expected = "/path/to/file.mov"
        seqs = FileSequence(expected)

        self.assertEquals(expected, seqs.index(0))
        self.assertEquals(expected, seqs.frame(0))
        self.assertEquals(expected, seqs[0])
        self.assertEquals(None, seqs.frameSet())
        self.assertEquals("", seqs.frameRange())
        self.assertEquals("", seqs.invertedFrameRange())
        self.assertEquals(1, len(seqs))

        seqs.setFrameRange("1-100")

        for i in range(0, 100):
            self.assertEquals(expected, seqs.index(i))
            self.assertEquals(expected, seqs.frame(i + 1))
            self.assertEquals(expected, seqs[i])
        self.assertEquals(1, len(seqs))

        seqs.setPadding("#")
        self.assertEquals(100, len(seqs))

    def testNoPlaceholderNumExt(self):
        basename = 'file'
        exts = ('.7zip', '.mp4')

        for ext in exts:
            expected = basename + ext
            seqs = FileSequence(expected)

            self.assertEquals(ext, seqs.extension())
            self.assertEquals(basename, seqs.basename())
            self.assertEquals(expected, str(seqs))

    def testSplitXY(self):
        seqs = FileSequence("/cheech/0-9x1/chong.1-10#.exr")
        self.assertEquals("/cheech/0-9x1/chong.0001.exr", seqs.index(0))

    def testSerialization(self):
        fs = FileSequence("/path/to/file.1-100x2#.exr")
        s = pickle.dumps(fs, pickle.HIGHEST_PROTOCOL)
        fs2 = pickle.loads(s)
        self.assertEquals(str(fs), str(fs2))
        self.assertEquals(len(fs), len(fs2))
        self.assertEquals(list(fs), list(fs2))

        fs = FileSequence("/path/to/file.1-100x2%04d.exr")
        s = pickle.dumps(fs, pickle.HIGHEST_PROTOCOL)
        fs2 = pickle.loads(s)
        self.assertEquals(str(fs), str(fs2))
        self.assertEquals(len(fs), len(fs2))
        self.assertEquals(list(fs), list(fs2))

    def testSerializationCompatablity(self):
        fs = FileSequence(PICKLE_TEST_SEQ)
        for version, s in OLD_PICKLE_MAP.items():
            fs2 = pickle.loads(s)
            self.assertEquals(str(fs), str(fs2))
            self.assertEquals(len(fs), len(fs2))
            self.assertEquals(list(fs), list(fs2))

    def testSerializationJSON(self):
        fs = FileSequence("/path/to/file.1-100x0.25#.@@.exr",
                          pad_style=constants.PAD_STYLE_HASH1,
                          allow_subframes=True)
        s = json.dumps(fs.to_dict())
        fs2 = FileSequence.from_dict(json.loads(s))
        self.assertEquals(str(fs), str(fs2))
        self.assertEquals(len(fs), len(fs2))
        self.assertEquals(list(fs), list(fs2))
        self.assertEquals(fs.frameSet(), fs2.frameSet())
        self.assertEquals(fs.padStyle(), fs2.padStyle())
        self.assertEquals(fs.__dict__, fs2.__dict__)

    def testHasVersionNoFrame(self):
        for allow_subframes in [False, True]:
            fs = FileSequence("/path/to/file_v2.exr", allow_subframes=allow_subframes)
            self.assertEquals(fs.start(), 0)
            self.assertEquals(fs.end(), 0)
            self.assertEquals(fs.padding(), '')
            self.assertEquals(fs.framePadding(), '')
            self.assertEquals(fs.subframePadding(), '')
            self.assertEquals(fs.extension(), '.exr')
            self.assertEquals(str(fs), "/path/to/file_v2.exr")

    def testHasFrameNoVersion(self):
        for allow_subframes in [False, True]:
            fs = FileSequence("/path/to/file.2.exr", allow_subframes=allow_subframes)
            self.assertEquals(fs.start(), 2)
            self.assertEquals(fs.end(), 2)
            self.assertEquals(fs.padding(), '@')
            self.assertEquals(fs.framePadding(), '@')
            self.assertEquals(fs.subframePadding(), '')
            self.assertEquals(fs.extension(), '.exr')
            self.assertEquals(str(fs), "/path/to/file.2@.exr")

    def testHasSubFrameNoVersion(self):
        fs = FileSequence("/path/to/file.0.0005.exr", allow_subframes=True)
        self.assertEquals(fs.start(), Decimal("0.0005"))
        self.assertEquals(fs.end(), Decimal("0.0005"))
        self.assertEquals(fs.padding(), '@.#')
        self.assertEquals(fs.framePadding(), '@')
        self.assertEquals(fs.subframePadding(), '#')
        self.assertEquals(fs.extension(), '.exr')
        self.assertEquals(str(fs), "/path/to/file.0.0005@.#.exr")

    def testHasFrameResolution(self):
        for allow_subframes in [False, True]:
            fs = FileSequence(
                "/path/to/file.1920x1038.1001-1076#.exr", allow_subframes=allow_subframes
            )
            self.assertEquals(fs.start(), 1001)
            self.assertEquals(fs.end(), 1076)
            self.assertEquals(fs.padding(), '#')
            self.assertEquals(fs.framePadding(), '#')
            self.assertEquals(fs.subframePadding(), '')
            self.assertEquals(fs.extension(), '.exr')
            self.assertEquals(str(fs), "/path/to/file.1920x1038.1001-1076#.exr")

    def testHasFrameListResolution(self):
        for allow_subframes in [False, True]:
            fs = FileSequence(
                "/path/to/file.1920x1038.1001,1005,1076#.exr", allow_subframes=allow_subframes
            )
            self.assertEquals(fs.start(), 1001)
            self.assertEquals(fs.end(), 1076)
            self.assertEquals(fs.padding(), '#')
            self.assertEquals(fs.framePadding(), '#')
            self.assertEquals(fs.subframePadding(), '')
            self.assertEquals(fs.extension(), '.exr')
            self.assertEquals(str(fs), "/path/to/file.1920x1038.1001,1005,1076#.exr")

    def testHasSubFrameResolution(self):
        fs = FileSequence("/path/to/file.1920x1038.1001-1002x0.25@.#.exr", allow_subframes=True)
        self.assertEquals(fs.start(), Decimal("1001.0"))
        self.assertEquals(fs.end(), Decimal("1002.0"))
        self.assertEquals(fs.padding(), '@.#')
        self.assertEquals(fs.framePadding(), '@')
        self.assertEquals(fs.subframePadding(), '#')
        self.assertEquals(fs.extension(), '.exr')
        self.assertEquals(str(fs), "/path/to/file.1920x1038.1001-1002x0.25@.#.exr")

    def testNoFrameNoVersionNoExt(self):
        for allow_subframes in [False, True]:
            fs = FileSequence("/path/to/file", allow_subframes=allow_subframes)
            self.assertEquals(fs.start(), 0)
            self.assertEquals(fs.end(), 0)
            self.assertEquals(fs.padding(), '')
            self.assertEquals(fs.dirname(), '/path/to/')
            self.assertEquals(fs.basename(), 'file')
            self.assertEquals(fs.extension(), '')
            self.assertEquals(str(fs), "/path/to/file")

            fs = FileSequence("file", allow_subframes=allow_subframes)
            self.assertEquals(fs.start(), 0)
            self.assertEquals(fs.end(), 0)
            self.assertEquals(fs.padding(), '')
            self.assertEquals(fs.dirname(), '')
            self.assertEquals(fs.basename(), 'file')
            self.assertEquals(fs.extension(), '')
            self.assertEquals(str(fs), "file")

    def testEmptyBasename(self):
        seq = FileSequence("/path/to/1-5#.exr")
        self.assertEquals(seq.basename(), "")
        self.assertEquals(len(seq), 5)
        self.assertEquals(seq.padding(), '#')

        seq = FileSequence("/path/to/1-5%04d.exr")
        self.assertEquals(seq.basename(), "")
        self.assertEquals(len(seq), 5)
        self.assertEquals(seq.padding(), '%04d')

        seq = FileSequence("/path/to/1-5$F4.exr")
        self.assertEquals(seq.basename(), "")
        self.assertEquals(len(seq), 5)
        self.assertEquals(seq.padding(), "$F4")

        seq = FileSequence("/path/to/1-5<UDIM>.exr")
        self.assertEquals(seq.basename(), "")
        self.assertEquals(len(seq), 5)
        self.assertEquals(seq.padding(), "<UDIM>")

    def testStringSubclasses(self):
        def sep(p):
            return p.replace("/", os.sep)

        tests = [
            ("/path/to/files.0001.ext", sep("/path/to/"), "files."),
            ("/path/to/files.1-100#.ext", sep("/path/to/"), "files."),
            ("/path/to/files.ext", sep("/path/to/"), "files"),
            ("/path/to/files", sep("/path/to/"), "files"),
        ]
        for path, dirname, basename in tests:
            fs = FileSequence(_CustomPathString(path))
            self.assertTrue(fs.dirname() == dirname,
                            "Expected '%s', got '%s' (with %s)" % (dirname, fs.dirname(), path))
            self.assertTrue(fs.basename() == basename,
                            "Expected '%s', got '%s' (with %s)" % (basename, fs.basename(), path))

    def test_yield_sequences_in_list(self):
        self._test_yield_sequences_in_list()

    def test_yield_sequences_in_list_win(self):
        sep = r'\\'
        self._test_yield_sequences_in_list(sep)

    def _test_yield_sequences_in_list(self, sep='/'):
        paths = [
            '/path/to/file20.v123.5.png',
            '/path/to/file20.v123.1.exr',
            '/path/to/file20.v123.2.exr',
            '/path/to/file20.v123.3.exr',
            '/path/to/.cruft.file',
            '/path/to/.cruft',
            '/path/to/file2.exr',
            '/path/to/file2.7zip',
            '/path/to/file.2.7zip',
            '/path/to/file.3.7zip',
            '/path/to/file.4.7zip',
            '/path/to/file.4.mp4',
            '',  # empty path test
            "mixed_seqs/file5.ext",
            "mixed_seqs/file20.ext",
            "mixed_seqs/file30.ext",
            "mixed_seqs/no_ext",
            "mixed_seqs/no_ext.200,300@@@",
            "mixed_seqs/no_ext_10",
            "mixed_seqs/not_a_seq.ext",
            "mixed_seqs/seq.0001.ext",
            "mixed_seqs/seq.0002.ext",
            "mixed_seqs/seq.0003.ext",
            "mixed_seqs/seq2a.1.ext",
            "mixed_seqs/seq2a.2.ext",
            "mixed_seqs/seq2a.3.ext",
            "/path/to/file4-4.exr",
            "/path/to/file4-5.exr",
            "/path/to/file--4.exr",
            "path/01.exr",
            "path/02.exr",
            "path/03.exr",
            "path/001.file",
            "path/002.file",
            "path/003.file",
            "path/0001.jpg",
            "path/0002.jpg",
            "path/0003.jpg",
            "2frames.01.jpg",
            "2frames.02.jpg",
            '8frames.01.jpg',
            '8frames.02.jpg',
            '8frames.05.jpg',
            '8frames.07.jpg',
            '8frames.08.jpg',
            '8frames.10.jpg',
            '8frames.11.jpg',

            # Issue 94: ensure original padding is observed
            'mixed_pad/file.004.jpg',
            'mixed_pad/file.08.jpg',
            'mixed_pad/file.009.jpg',
            'mixed_pad/file.015.jpg',

            # Issue 107: mixed case
            'mixed_case/sub/file_foo_001.ext',
            'mixed_case/sub/file_foo_002.ext',
            'mixed_case/sub/file_foo_003.ext',
            'mixed_case/sub/file_FOO_004.ext',
            'mixed_case/sub/file_FOO_005.ext',
            'mixed_case/sub/file_FOO_006.ext',
        ]

        expected = {
            '/path/to/file2@.7zip',
            '/path/to/file20.v123.1-3@.exr',
            '/path/to/file.2-4@.7zip',
            '/path/to/file2@.exr',
            '/path/to/file.4@.mp4',
            '/path/to/.cruft.file',
            '/path/to/.cruft',
            '/path/to/file20.v123.5@.png',
            "mixed_seqs/file5,20,30@.ext",
            "mixed_seqs/seq2a.1-3@.ext",
            "mixed_seqs/seq.1-3#.ext",
            "mixed_seqs/not_a_seq.ext",
            "mixed_seqs/no_ext",
            "mixed_seqs/no_ext_10@@",
            "mixed_seqs/no_ext.200,300@@@",
            '/path/to/file4-5--4@@.exr',
            '/path/to/file--4@@.exr',
            'path/1-3@@.exr',
            'path/1-3@@@.file',
            'path/1-3#.jpg',
            '2frames.1-2@@.jpg',
            '8frames.1-2,5,7-8,10-11@@.jpg',

            # Issue 94: ensure original padding is observed
            'mixed_pad/file.8@@.jpg',
            'mixed_pad/file.4,9,15@@@.jpg',

            # Issue 107: mixed case
            'mixed_case/sub/file_foo_1-3@@@.ext',
            'mixed_case/sub/file_FOO_4-6@@@.ext',
        }

        sub = self.RX_PATHSEP.sub
        paths = [sub(sep, p) for p in paths]
        expected = {sub(sep, p) for p in expected}

        actual = set(str(fs) for fs in FileSequence.yield_sequences_in_list(paths))
        self.assertEquals(expected, actual)

        paths = list(map(_CustomPathString, paths))
        actual = set(str(fs) for fs in FileSequence.yield_sequences_in_list(paths))
        self.assertEquals({str(_CustomPathString(p)) for p in expected}, actual)

    def test_yield_sequences_in_list_using(self):
        paths = [
            'seq/file_0003.0001.exr',
            'seq/file_0005.0001.exr',
            'seq/file_0007.0001.exr',
        ]

        expects = [os.path.join("seq", "file_3-7x2#.0001.exr")]

        template = FileSequence('seq/file_@@.0001.exr')
        actual = {str(fs) for fs in FileSequence.yield_sequences_in_list(paths, using=template)}

        for expect in expects:
            self.assertIn(expect, actual)

        expects = [
            "seq/file_0003.1#.exr",
            "seq/file_0005.1#.exr",
            "seq/file_0007.1#.exr",
        ]

        actual = {str(fs) for fs in FileSequence.yield_sequences_in_list(paths)}

        for expect in expects:
            self.assertIn(expect, actual)

    def test_yield_sequences_in_list_multi_pad(self):
        paths = [
            'mixed_pad/file.004.jpg',
            'mixed_pad/file.08.jpg',
            'mixed_pad/file.009.jpg',
            'mixed_pad/file.0013.jpg',
            'mixed_pad/file.015.jpg',
            'mixed_pad/file.0015.jpg',
            'mixed_pad/file.0014.jpg',
        ]

        expects = [
            'mixed_pad/file.8##.jpg',
            'mixed_pad/file.4,9,15###.jpg',
            'mixed_pad/file.13-15####.jpg',
        ]
        actual = {str(fs) for fs in FileSequence.yield_sequences_in_list(paths, pad_style=constants.PAD_STYLE_HASH1)}
        for expect in expects:
            self.assertIn(expect, actual)

        expects = [
            'mixed_pad/file.8@@.jpg',
            'mixed_pad/file.4,9,15@@@.jpg',
            'mixed_pad/file.13-15#.jpg',
        ]
        actual = {str(fs) for fs in FileSequence.yield_sequences_in_list(paths, pad_style=constants.PAD_STYLE_HASH4)}
        for expect in expects:
            self.assertIn(expect, actual)

    def test_yield_sequences_in_list_pad_style(self):
        paths = [
            'seq/file.0001.exr',
            'seq/file.0002.exr',
            'seq/file.0003.exr',
        ]

        expect = 'seq/file.1-3#.exr'
        actual = list(FileSequence.yield_sequences_in_list(paths, pad_style=fileseq.PAD_STYLE_HASH4))[0]
        self.assertEqual(expect, str(actual))
        self.assertEqual(fileseq.PAD_STYLE_HASH4, actual.padStyle())
        self.assertEqual(4, actual.zfill())

        expect = 'seq/file.1-3####.exr'
        actual = list(FileSequence.yield_sequences_in_list(paths, pad_style=fileseq.PAD_STYLE_HASH1))[0]
        self.assertEqual(expect, str(actual))
        self.assertEqual(fileseq.PAD_STYLE_HASH1, actual.padStyle())
        self.assertEqual(4, actual.zfill())

    def testIgnoreFrameSetStrings(self):
        for char in "xy:,".split():
            fs = FileSequence("/path/to/file{0}1-1x1#.exr".format(char))
            self.assertEquals(fs.basename(), "file{0}".format(char))
            self.assertEquals(fs.start(), 1)
            self.assertEquals(fs.end(), 1)
            self.assertEquals(fs.padding(), '#')
            self.assertEquals(str(fs), "/path/to/file{0}1-1x1#.exr".format(char))

    def testStrUnicode(self):
        """
        https://github.com/justinfx/fileseq/issues/99
        https://github.com/justinfx/fileseq/issues/100
        """
        def check(seq):
            # make sure none of these raise a unicode exception
            s = str(seq)
            _ = repr(seq)
            _ = seq.format()

        utf8 = u'file_カ_Z.01.txt'
        latin1 = b'/proj/kenny/fil\xe9'
        latin1_to_utf8 = latin1.decode('latin1').encode(utils.FILESYSTEM_ENCODING)

        check(FileSequence(utf8))
        check(FileSequence(utf8.encode(utils.FILESYSTEM_ENCODING)))
        check(FileSequence(latin1_to_utf8))
        try:
            check(FileSequence(latin1))
        except UnicodeDecodeError:
            # Windows os.fsdecode() uses 'strict' error handling
            # instead of 'surrogateescape'. So just assume bytes
            # decoding error is expected for this case.
            if os.name != 'nt':
                raise

    def testBatchesPaths(self):
        Case = namedtuple('Case', ['input', 'batch', 'expect'])
        table = [
            Case("f.1-3@.x", 0, []),
            Case("f.1-3@.x", 1, [['f.1.x'], ['f.2.x'], ['f.3.x']]),
            Case("f.1-3@.x", 2, [['f.1.x','f.2.x'], ['f.3.x']]),
            Case("f.1-3@.x", 3, [['f.1.x','f.2.x','f.3.x']]),
            Case("f.1-3@.x", 9, [['f.1.x','f.2.x','f.3.x']]),
        ]

        for case in table:
            fs = FileSequence(case.input)
            actual = [list(i) for i in fs.batches(case.batch, paths=True)]
            self.assertEqual(case.expect, actual, msg=str(case))

    def testBatches(self):
        Case = namedtuple('Case', ['input', 'batch', 'expect'])
        table = [
            Case("f.1-3@.x", 0, []),
            Case("f.1-3@.x", 1, ['f.1@.x', 'f.2@.x', 'f.3@.x']),
            Case("f.1-3@.x", 2, ['f.1-2@.x', 'f.3@.x']),
            Case("f.1-3@.x", 3, ['f.1-3@.x']),
            Case("f.1-3@.x", 9, ['f.1-3@.x']),

            Case("f.1-3,10-16x2@@.x", 2, ['f.1-2@@.x', 'f.3,10@@.x', 'f.12,14@@.x', 'f.16@@.x']),
        ]

        for case in table:
            fs = FileSequence(case.input)
            expect = [FileSequence(i) for i in case.expect]
            actual = list(fs.batches(case.batch, paths=False))
            self.assertEqual(expect, actual, msg=str(case))


class TestFindSequencesOnDisk(TestBase):

    def testFindSequencesOnDisk(self):
        seqs = findSequencesOnDisk("seq", strictPadding=True)
        self.assertEquals(len(seqs), 10)

        known = {
            "seq/bar1000-1002,1004-1006#.exr",
            "seq/foo.1-5#.exr",
            "seq/foo.1-5#.jpg",
            "seq/foo.debug.1-5#.exr",
            "seq/foo_1#.exr",
            "seq/foo_0001_extra.exr",
            "seq/1-3#.exr",
            "seq/baz_left.1-3#.exr",
            "seq/baz_right.1-3#.exr",
            "seq/big.999-1003#.ext",
        }
        found = set([str(s) for s in seqs])
        self.assertEqualPaths(found, known)

    def testStrictPadding(self):
        tests = [
            ("seq/bar#.exr", ["seq/bar1000-1002,1004-1006#.exr"]),
            ("seq/bar@@@@.exr", ["seq/bar1000-1002,1004-1006@@@@.exr"]),
            ("seq/bar@@@.exr", ["seq/bar1000-1002,1004-1006@@@.exr"]),
            ("seq/bar@@.exr", ["seq/bar1000-1002,1004-1006@@.exr"]),
            ("seq/bar@.exr", ["seq/bar1000-1002,1004-1006@.exr"]),
            ("seq/bar@@@@@.exr", []),
            ("seq/bar#@.exr", []),
            ("seq/foo.#.exr", ["seq/foo.1-5#.exr"]),
            ("seq/foo.#.jpg", ["seq/foo.1-5#.jpg"]),
            ("seq/foo.#.exr", ["seq/foo.1-5#.exr"]),
            ("seq/foo.debug.#.exr", ["seq/foo.debug.1-5#.exr"]),
            ("seq/#.exr", ["seq/1-3#.exr"]),
            ("seq/foo_#.exr", ["seq/foo_1#.exr"]),
            ("seq/foo_#_extra.exr", []),
            ("seq/foo_##.exr", []),
            ("seq/foo_@.exr", []),
            ("seq/foo_#@.exr", []),
            ("seq/foo_@@_extra.exr", []),
            ("seq/baz_{left,right}.#.exr", ["seq/baz_left.1-3#.exr", "seq/baz_right.1-3#.exr"]),
            ("seq/baz_{left,right}.@@@@.exr", ["seq/baz_left.1-3@@@@.exr", "seq/baz_right.1-3@@@@.exr"]),
            ("seq/baz_{left,right}.@@@.exr", []),
            ("seq/baz_{left,right}.#@.exr", []),
        ]

        for pattern, expected in tests:
            seqs = findSequencesOnDisk(pattern, strictPadding=True)
            for seq in seqs:
                self.assertTrue(isinstance(seq, FileSequence))
            actual = self.toNormpaths([str(seq) for seq in seqs])
            expected = self.toNormpaths(expected)
            self.assertEqual(actual, expected)

    def testNegSequencesOnDisk(self):
        seqs = findSequencesOnDisk("seqneg")
        self.assertEquals(1, len(seqs))

    def testFindSequencesOnDiskNegative(self):
        seqs = findSequencesOnDisk("seqneg")
        self.assertEquals("seqneg/bar.-1-1#.exr", str(seqs[0]))
        self.assertEquals("seqneg/bar.-001.exr", seqs[0].frame(-1))
        self.assertEquals("seqneg/bar.-1001.exr", seqs[0].frame(-1001))
        self.assertEquals("seqneg/bar.-10011.exr", seqs[0].frame(-10011))
        self.assertEquals("seqneg/bar.1000.exr", seqs[0].frame(1000))

    def testFindSequencesOnDiskSkipHiddenFiles(self):
        seqs = findSequencesOnDisk("seqhidden")
        self.assertEquals(3, len(seqs))

        known = set(self.toNormpaths([
            "seqhidden/bar1000-1002,1004-1006#.exr",
            "seqhidden/foo.1-5#.exr",
            "seqhidden/foo.1-5#.jpg",
        ]))
        found = set(self.toNormpaths([str(s) for s in seqs]))
        self.assertEqual(known, found)
        self.assertFalse(known.difference(found))

    def testFindSequencesOnDiskIncludeHiddenFiles(self):
        seqs = findSequencesOnDisk("seqhidden", include_hidden=True)
        self.assertEquals(7, len(seqs))

        known = {
            "seqhidden/bar1000-1002,1004-1006#.exr",
            "seqhidden/.bar1000-1002,1004-1006#.exr",
            "seqhidden/foo.1-5#.exr",
            "seqhidden/.foo.1-5#.exr",
            "seqhidden/foo.1-5#.jpg",
            "seqhidden/.foo.1-5#.jpg",
            "seqhidden/.hidden",
        }
        found = set([str(s) for s in seqs])
        self.assertEqualPaths(known, found)

    def testCrossPlatformPathSep(self):
        expected = {
            "seqsubdirs/sub1/1-3#.exr",
            "seqsubdirs/sub1/bar1000-1002,1004-1006#.exr",
            "seqsubdirs/sub1/foo.1-5#.exr",
            "seqsubdirs/sub1/foo.1-5#.jpg",
            "seqsubdirs/sub1/foo.debug.1-5#.exr",
            "seqsubdirs/sub1/foo_1#.exr",
        }

        import ntpath
        _join = os.path.join
        os.path.join = ntpath.join

        try:
            self.assertEqual(os.path.join('a', 'b'), 'a\\b')
            seqs = findSequencesOnDisk("seqsubdirs/sub1")

            self.assertEquals(len(expected), len(seqs))

            actual = set(str(s) for s in seqs)
            self.assertEqual(actual, expected)

        finally:
            os.path.join = _join

    def testStrictPaddingSubFrameSeq(self):
        tests = [
            ("subframe_seq/foo.#.#.jpg", ['subframe_seq/foo.1-3x0.25#.#.jpg']),
            ("subframe_seq/foo.#.#.exr", ['subframe_seq/foo.1-3x0.25#.#.exr']),
            ("subframe_seq/foo.@@@@.@@@@.exr", ['subframe_seq/foo.1-3x0.25@@@@.@@@@.exr']),
            ("subframe_seq/foo.@@@.@@@@.exr", []),
            ("subframe_seq/foo.@@.@@@@.exr", []),
            ("subframe_seq/foo.@.@@@@.exr", []),
            ("subframe_seq/foo.@@@.@@@.exr", []),
            ("subframe_seq/foo.@@.@@.exr", []),
            ("subframe_seq/foo.@.@.exr", []),
            ("subframe_seq/foo.@@@@@.@@@@@.exr", []),

            ("subframe_seq/foz.#.#.exr", ['subframe_seq/foz.1001-1003x0.25#.#.exr']),
            ("subframe_seq/foz.@@@@.@@@@.exr", ['subframe_seq/foz.1001-1003x0.25@@@@.@@@@.exr']),
            ("subframe_seq/foz.@@@@.@@@.exr", []),
            ("subframe_seq/foz.@@@@.@@.exr", []),
            ("subframe_seq/foz.@@@@.@.exr", []),
            ("subframe_seq/foz.@@@@.#.exr", ['subframe_seq/foz.1001-1003x0.25@@@@.#.exr']),
            ("subframe_seq/foz.@@@.#.exr", ['subframe_seq/foz.1001-1003x0.25@@@.#.exr']),
            ("subframe_seq/foz.@@.#.exr", ['subframe_seq/foz.1001-1003x0.25@@.#.exr']),
            ("subframe_seq/foz.@.#.exr", ['subframe_seq/foz.1001-1003x0.25@.#.exr']),

            ("subframe_seq/foz.debug.#.#.exr", ['subframe_seq/foz.debug.1001-1002x0.25#.#.exr']),

            ("subframe_seq/baz_{left,right}.#.#.exr", ['subframe_seq/baz_left.1001-1002x0.25#.#.exr', 'subframe_seq/baz_right.1001-1002x0.25#.#.exr']),
        ]

        for pattern, expected in tests:
            seqs = findSequencesOnDisk(pattern, strictPadding=True, allow_subframes=True)
            for seq in seqs:
                self.assertTrue(isinstance(seq, FileSequence))
            actual = self.toNormpaths([str(seq) for seq in seqs])
            expected = self.toNormpaths(expected)
            self.assertEqual(expected, actual)

    def testFindSequencesOnDiskSubFrames(self):
        seqs = findSequencesOnDisk("subframe_seq", allow_subframes=True)
        self.assertEquals(9, len(seqs))
        known = {
            'subframe_seq/bar.1#.#.exr',
            'subframe_seq/baz.1-2x0.25,3-4x0.25#.#.exr',
            'subframe_seq/baz_left.1001-1002x0.25#.#.exr',
            'subframe_seq/baz_right.1001-1002x0.25#.#.exr',
            'subframe_seq/foo.1-3x0.25#.#.exr',
            'subframe_seq/foo.1-3x0.25#.#.jpg',
            'subframe_seq/foz.1001-1003x0.25#.#.exr',
            'subframe_seq/foz.debug.1001-1002x0.25#.#.exr',
            'subframe_seq/guz.1-2x0.25#.@@.exr'
        }
        found = set([str(s) for s in seqs])
        self.assertEqualPaths(known, found)

    def testFindSequencesOnDiskNegativeSubFrames(self):
        seqs = findSequencesOnDisk("subframe_seqneg", allow_subframes=True)
        self.assertEquals("subframe_seqneg/bar.-0.5-0.5x0.5#.#.exr", str(seqs[0]))
        self.assertEquals("subframe_seqneg/bar.-001.5000.exr", seqs[0].frame("-1.5"))
        self.assertEquals("subframe_seqneg/bar.0001.5000.exr", seqs[0].frame("1.5"))
        self.assertEquals("subframe_seqneg/bar.0001.5000.exr", seqs[0].frame(1.5))
        self.assertEquals("subframe_seqneg/bar.-1001.0000.exr", seqs[0].frame(Decimal("-1001.0000")))
        self.assertEquals("subframe_seqneg/bar.-1001.0000.exr", seqs[0].frame(Decimal("-1001.0")))
        self.assertEquals("subframe_seqneg/bar.-1001.0000.exr", seqs[0].frame(Decimal(-1001.0)))
        self.assertEquals("subframe_seqneg/bar.-1001.0000.exr", seqs[0].frame(Decimal("-1001")))
        self.assertEquals("subframe_seqneg/bar.-1001.0000.exr", seqs[0].frame(Decimal(-1001)))

    def testCaseSensitive(self):
        """Issue 107 - testing case-sensitive matching between windows/linux"""
        # posix platforms are case-sensitive
        tests = [
            ('mixed_case/sub/file_foo_*.ext', ['mixed_case/sub/file_foo_1-3@@@.ext']),
            ('mixed_case/sub/file_FOO_*.ext', ['mixed_case/sub/file_FOO_4-6@@@.ext']),
            ('mixed_case/sub/file_*_*.ext', ['mixed_case/sub/file_foo_1-3@@@.ext', 'mixed_case/sub/file_FOO_4-6@@@.ext']),
        ]

        for pattern, expected in tests:
            actual = findSequencesOnDisk(pattern)
            self.assertEqual(len(expected), len(actual))

            actual = self.toNormpaths([str(s) for s in actual])
            expected = self.toNormpaths(expected)
            self.assertEqual(expected, actual)


class TestFindSequenceOnDisk(TestBase):

    def testFindSequenceOnDisk(self):
        tests = [
            ("seq/bar#.exr", "seq/bar1000-1002,1004-1006#.exr"),
            ("seq/foo.#.exr", "seq/foo.1-5#.exr"),
            ("seq/foo.#.jpg", "seq/foo.1-5#.jpg"),
            ("seq/foo.0002.jpg", "seq/foo.1-5#.jpg"),
            ("seq/foo.debug.#.exr", "seq/foo.debug.1-5#.exr"),
            ("seq/#.exr", "seq/1-3#.exr"),
            ("seq/bar1001.exr", "seq/bar1001.exr"),
            ("seq/foo_0001.exr", "seq/foo_0001.exr"),
            ("multi_range/file_#.0001.exr", "multi_range/file_3-5#.0001.exr"),
            ("subframe_seq/baz.#.0000.exr", "subframe_seq/baz.1-4#.0000.exr"),
            ("subframe_seq/baz.0001.#.exr", "subframe_seq/baz.0001.0-7500x2500#.exr"),
            ("subframe_seq/baz.0001.0000.exr", "subframe_seq/baz.0001.0-7500x2500#.exr"),
        ]

        for pattern, expected in tests:
            seq = findSequenceOnDisk(pattern, strictPadding=False)
            self.assertTrue(isinstance(seq, FileSequence))
            actual = str(seq)
            self.assertEqual(actual, expected)

    def testFindSequenceOnDiskNoMatch(self):
        tests = [
            "subframe_seq/baz.#.exr",
            "subframe_seq/baz.1000.exr",
        ]

        for pattern in tests:
            with self.assertRaises(FileSeqException) as cm:
                findSequenceOnDisk(pattern, strictPadding=False)
            self.assertEqual(str(cm.exception), 'no sequence found on disk matching ' + pattern)

    def testFindSequenceOnDiskSubFrames(self):
        tests = [
            ("seq/foo.#.exr", "seq/foo.1-5#.exr"),
            ("seq/foo.#.jpg", "seq/foo.1-5#.jpg"),
            ("seq/foo.0002.jpg", "seq/foo.1-5#.jpg"),
            ("subframe_seq/baz.#.#.exr", "subframe_seq/baz.1-2x0.25,3-4x0.25#.#.exr"),
            ("subframe_seq/baz.0000.0000.exr", "subframe_seq/baz.1-2x0.25,3-4x0.25#.#.exr"),
        ]

        for pattern, expected in tests:
            seq = findSequenceOnDisk(pattern, strictPadding=False, allow_subframes=True)
            self.assertTrue(isinstance(seq, FileSequence))
            actual = str(seq)
            self.assertEqual(actual, expected)

    def testStrictPadding(self):
        tests = [
            ("seq/bar#.exr", "seq/bar1000-1002,1004-1006#.exr"),
            ("seq/bar@@@@.exr", "seq/bar1000-1002,1004-1006@@@@.exr"),
            ("seq/bar@@@.exr", "seq/bar1000-1002,1004-1006@@@.exr"),
            ("seq/bar@@.exr", "seq/bar1000-1002,1004-1006@@.exr"),
            ("seq/bar@.exr", "seq/bar1000-1002,1004-1006@.exr"),
            ("seq/bar@@@@@.exr", None),
            ("seq/bar#@.exr", None),
            ("seq/foo.#.exr", "seq/foo.1-5#.exr"),
            ("seq/foo.#.jpg", "seq/foo.1-5#.jpg"),
            ("seq/foo.#.exr", "seq/foo.1-5#.exr"),
            ("seq/foo.debug.#.exr", "seq/foo.debug.1-5#.exr"),
            ("seq/#.exr", "seq/1-3#.exr"),
            ("seq/foo_#.exr", "seq/foo_1#.exr"),
            ("seq/foo_#_extra.exr", None),
            ("seq/foo_##.exr", None),
            ("seq/foo_@.exr", None),
            ("seq/big.#.ext", "seq/big.999-1003#.ext"),
            ("seq/big.@@@.ext", "seq/big.1000-1003@@@.ext"),
            ("seq/big.@.ext", "seq/big.1000-1003@.ext"),
            ("seq/big.#@.ext", None),
            ("multi_range/file_@@.0001.exr", None),
            ("multi_range/file_#.0001.exr", "multi_range/file_3-5#.0001.exr"),
        ]

        for pattern, expected in tests:
            if expected is None:
                with self.assertRaises(FileSeqException, msg=pattern):
                    findSequenceOnDisk(pattern, strictPadding=True)
                continue

            seq = findSequenceOnDisk(pattern, strictPadding=True)
            self.assertTrue(isinstance(seq, FileSequence))
            actual = str(seq)
            self.assertEqual(actual, expected)

    def testCrossPlatformPathSep(self):
        tests = [
            ("seq/bar#.exr", "seq\\bar1000-1002,1004-1006#.exr"),
            ("seq/foo.#.exr", "seq\\foo.1-5#.exr"),
            ("seq/foo.#.jpg", "seq\\foo.1-5#.jpg"),
            ("seq/foo.0002.jpg", "seq\\foo.1-5#.jpg"),
            ("seq/foo.#.exr", "seq\\foo.1-5#.exr"),
            ("seq/foo.debug.#.exr", "seq\\foo.debug.1-5#.exr"),
            ("seq/#.exr", "seq\\1-3#.exr"),
            ("seq/bar1001.exr", "seq/bar1001.exr"),
            ("seq/foo_0001.exr", "seq/foo_0001.exr"),
        ]

        import ntpath
        _path = os.path
        os.path = ntpath

        try:
            self.assertEqual(os.path.join('a', 'b'), 'a\\b')

            for pattern, expected in tests:
                seq = findSequenceOnDisk(pattern)
                self.assertTrue(isinstance(seq, FileSequence))
                actual = str(seq)
                self.assertEqual(actual, expected)

        finally:
            os.path = _path

    def testPaddingMatch(self):
        tests = [
            ("mixed/seq.#.ext", "mixed/seq.-1-5#.ext"),
            ("mixed/seq.@@.ext", "mixed/seq.-1-5@@.ext"),
            ("mixed/seq.@@@@@.ext", "mixed/seq.-1-5@@@@@.ext"),
            ("mixed/seq.@.ext", "mixed/seq.-1@.ext"),
            ("mixed/seq.##.ext", None),
            ("mixed/seq.%04d.ext", "mixed/seq.-1-5#.ext"),
            ("mixed/seq.%02d.ext", "mixed/seq.-1-5@@.ext"),
            ("mixed/seq.%05d.ext", "mixed/seq.-1-5@@@@@.ext"),
            ("mixed/seq.%01d.ext", "mixed/seq.-1@.ext"),
            ("mixed/seq.%08d.ext", None),
        ]

        for pattern, expected in tests:
            if expected is None:
                with self.assertRaises(FileSeqException):
                    findSequenceOnDisk(pattern, strictPadding=True)
                continue

            seq = findSequenceOnDisk(pattern, strictPadding=True)
            self.assertTrue(isinstance(seq, FileSequence))

            actual = str(seq)
            self.assertEqual(actual, expected)

    def testCaseSensitive(self):
        """Issue 107 - testing case-sensitive matching between windows/linux"""
        # posix platforms are case-sensitive
        if sys.platform == 'win32':
            tests = [
                ('mixed_case\\sub\\file_foo_#.ext', 'mixed_case\\sub\\file_foo_1-3@@@.ext'),
                ('mixed_case\\sub\\file_FOO_#.ext', 'mixed_case\\sub\\file_FOO_4-6@@@.ext'),
                ('mixed_case/sub/file_foo_#.ext', 'mixed_case/sub/file_foo_1-3@@@.ext'),
                ('mixed_case/sub/file_FOO_#.ext', 'mixed_case/sub/file_FOO_4-6@@@.ext'),
                ('mixed_case/sub\\file_FOO_#.ext', 'mixed_case/sub/file_FOO_4-6@@@.ext'),
                ('mixed_case\\sub/file_FOO_#.ext', 'mixed_case\\sub\\file_FOO_4-6@@@.ext'),
            ]
        else:
            tests = [
                ('mixed_case/sub/file_foo_#.ext', 'mixed_case/sub/file_foo_1-3@@@.ext'),
                ('mixed_case/sub/file_FOO_#.ext', 'mixed_case/sub/file_FOO_4-6@@@.ext'),
            ]

        for pattern, expected in tests:
            seq = findSequenceOnDisk(pattern)
            unittest.TestCase.assertTrue(self, isinstance(seq, FileSequence))

            actual = str(seq)
            unittest.TestCase.assertEqual(self, expected, actual)

    def testPreservePadding(self):
        class Case(object):
            def __init__(self, pat, expected, strict=False):
                self.pat = pat
                self.expected = expected
                self.strict = strict
            def __repr__(self):
                return "Case(pat={!r}, expect={!r}, strict={})".format(self.pat, self.expected, self.strict)

        strict_tests = [
            Case("seq/big.@.ext", "seq/big.1000-1003@.ext", strict=True),
            Case("seq/big.@@.ext", "seq/big.1000-1003@@.ext", strict=True),
            Case("seq/big.@@@.ext", "seq/big.1000-1003@@@.ext", strict=True),
            Case("seq/big.#.ext", "seq/big.999-1003#.ext", strict=True),
            Case("seq/big.#@.ext", None, strict=True),
            Case("seq/foo.#.exr", "seq/foo.1-5#.exr", strict=True),
            Case("seq/foo.@.exr", None, strict=True),

            Case("seq/big.%02d.ext", "seq/big.1000-1003%02d.ext", strict=True),
            Case("seq/big.%04d.ext", "seq/big.999-1003%04d.ext", strict=True),
            Case("seq/big.%08d.ext", None, strict=True),
            Case("seq/foo.%04d.exr", "seq/foo.1-5%04d.exr", strict=True),
            Case("seq/foo.%02d.exr", None, strict=True),

            Case("seq/big.$F.ext", "seq/big.1000-1003$F.ext", strict=True),
            Case("seq/big.$F4.ext", "seq/big.999-1003$F4.ext", strict=True),
            Case("seq/big.$F8.ext", None, strict=True),
            Case("seq/foo.$F4.exr", "seq/foo.1-5$F4.exr", strict=True),
            Case("seq/foo.$F.exr", None, strict=True),
        ]

        nonstrict_tests = [
            Case("seq/foo.@.exr", "seq/foo.1-5#.exr", strict=False),
            Case("seq/foo.@@.exr", "seq/foo.1-5#.exr", strict=False),
            Case("seq/foo.@@@.exr", "seq/foo.1-5#.exr", strict=False),
            Case("seq/foo.#.exr", "seq/foo.1-5#.exr", strict=False),
            Case("seq/foo.#@.exr", "seq/foo.1-5#.exr", strict=False),
            Case("seq/bar#@.exr", "seq/bar1000-1002,1004-1006#.exr", strict=False),

            Case("seq/foo.%04d.exr", "seq/foo.1-5%04d.exr", strict=False),
            Case("seq/foo.%02d.exr", "seq/foo.1-5#.exr", strict=False),
            Case("seq/foo.%08d.exr", "seq/foo.1-5#.exr", strict=False),
            Case("seq/bar%03d.exr", "seq/bar1000-1002,1004-1006%03d.exr", strict=False),
            Case("seq/bar%04d.exr", "seq/bar1000-1002,1004-1006%04d.exr", strict=False),
            Case("seq/bar%05d.exr", "seq/bar1000-1002,1004-1006#.exr", strict=False),

            Case("seq/foo.$F4.exr", "seq/foo.1-5$F4.exr", strict=False),
            Case("seq/foo.$F.exr", "seq/foo.1-5#.exr", strict=False),
            Case("seq/foo.$F3.exr", "seq/foo.1-5#.exr", strict=False),
            Case("seq/foo.$F8.exr", "seq/foo.1-5#.exr", strict=False),
            Case("seq/bar$F.exr", "seq/bar1000-1002,1004-1006$F.exr", strict=False),
            Case("seq/bar$F3.exr", "seq/bar1000-1002,1004-1006$F3.exr", strict=False),
            Case("seq/bar$F4.exr", "seq/bar1000-1002,1004-1006$F4.exr", strict=False),
            Case("seq/bar$F5.exr", "seq/bar1000-1002,1004-1006#.exr", strict=False),
        ]

        for case in strict_tests + nonstrict_tests:
            if case.expected is None:
                with self.assertRaises(FileSeqException, msg=case):
                    findSequenceOnDisk(case.pat, strictPadding=case.strict, preserve_padding=True)
                continue

            seq = findSequenceOnDisk(case.pat, strictPadding=case.strict, preserve_padding=True)
            self.assertTrue(isinstance(seq, FileSequence), msg=case)

            actual = str(seq)
            self.assertEqual(case.expected, actual, msg=case)


class TestPaddingFunctions(TestBase):
    """
    Test functions that help deal with padding on file sequences.
    """

    def testGetPaddingChars(self):
        """
        Ensure that we're getting back the proper padding characters.
        :return: None
        """
        cases = [
            (0, '@'),
            (1, '@'),
            (2, '@@'),
            (3, '@@@'),
            (4, '#'),
            (8, '##'),
        ]

        for case in cases:
            actual = getPaddingChars(case[0])
            self.assertEqual(actual, case[1])
            self.assertNativeStr(actual)

    def testGetPaddingNum(self):
        """
        Ensure that we're getting back the proper padding number.
        :return: None
        """
        self.assertEqual(getPaddingNum('@'), 1)
        self.assertEqual(getPaddingNum('@@'), 2)
        self.assertEqual(getPaddingNum('@@@'), 3)
        self.assertEqual(getPaddingNum('#'), 4)
        self.assertEqual(getPaddingNum('##'), 8)
        self.assertEqual(getPaddingNum('#@'), 5)
        self.assertEqual(getPaddingNum('##@@'), 10)

        self.assertEqual(getPaddingNum('%01d'), 1)
        self.assertEqual(getPaddingNum('%1d'), 1)
        self.assertEqual(getPaddingNum('%04d'), 4)
        self.assertEqual(getPaddingNum('%10d'), 10)
        self.assertEqual(getPaddingNum('%00d'), 1)
        self.assertEqual(getPaddingNum('%0d'), 1)
        self.assertEqual(getPaddingNum('%d'), 1)

        self.assertEqual(getPaddingNum('$F'), 1)
        self.assertEqual(getPaddingNum('$F1'), 1)
        self.assertEqual(getPaddingNum('$F2'), 2)
        self.assertEqual(getPaddingNum('$F3'), 3)

        self.assertEqual(getPaddingNum('<UDIM>'), 4)
        self.assertEqual(getPaddingNum('%(UDIM)d'), 4)

        allPossibleChars = [s for s in string.printable if s not in PAD_MAP]
        for char in allPossibleChars:
            self.assertRaises(ValueError, getPaddingNum, char)
            self.assertRaises(ValueError, getPaddingNum, '#{}'.format(char))
            self.assertRaises(ValueError, getPaddingNum, '@{}'.format(char))

        allPossibleChars = [s for s in string.printable if s not in PAD_MAP and s not in string.digits]
        for char in allPossibleChars:
            self.assertRaises(ValueError, getPaddingNum, '%{}d'.format(char))

        allPossibleChars = [s for s in string.printable if s not in PAD_MAP and s not in string.digits]
        for char in allPossibleChars:
            self.assertRaises(ValueError, getPaddingNum, '$F{}'.format(char))

    def testConformPadding(self):
        """
        Ensure that alternate padding formats are conformed
        to a primary PAD_MAP format
        """
        class Case(object):
            def __init__(self, src, expected, error=False):
                self.src = src
                self.expected = expected
                self.error = error

        tests = [
            Case('#', '#',),
            Case('#@', '#@'),
            Case('@@@@', '@@@@'),
            Case('@@@', '@@@'),
            Case('@@', '@@'),
            Case('@', '@'),
            Case('%08d', '##'),
            Case('%05d', '@@@@@'),
            Case('%04d', '#'),
            Case('%03d', '@@@'),
            Case('%02d', '@@'),
            Case('%01d', '@'),
            Case('%00d', '@'),
            Case('%1d', '@'),
            Case('%d', '@'),
            Case('$F', '@'),
            Case('$F1', '@'),
            Case('$F2', '@@'),
            Case('$F4', '#'),
            Case('<UDIM>', '#'),
            Case('%(UDIM)d', '#'),
            Case('', ''),
            Case('foo', 'foo', error=True),
        ]

        for case in tests:

            if case.error:
                with self.assertRaises(ValueError):
                    FileSequence.conformPadding(case.src)
                continue

            actual = FileSequence.conformPadding(case.src)
            self.assertEqual(actual, case.expected)
            self.assertNativeStr(actual)

    def testPadFrameRange(self):
        class Case(object):
            def __init__(self, frange, pad, expected):
                self.frange = frange
                self.pad = pad
                self.expected = expected
            def __str__(self):
                return "input={!r}, pad={}".format(self.frange, self.pad)

        tests = [
            Case('1', 6, '000001'),
            Case('-1', 6, '-00001'),
            Case('1-100', 6, '000001-000100'),
            Case('-1-100', 6, '-00001-000100'),
            Case('-1--100', 6, '-00001--00100'),
            Case('1--100', 6, '000001--00100'),
            Case('1-100x2', 6, '000001-000100x2'),
            Case('-1-100x2', 6, '-00001-000100x2'),
            Case('-1--100x2', 6, '-00001--00100x2'),
            Case('1--100x2', 6, '000001--00100x2'),
            Case('1--100x2', 5, '00001--0100x2'),
            Case('1--100x2', 4, '0001--100x2'),
            Case('1--100x2', 3, '001--100x2'),
            Case('1--100x2', 2, '01--100x2'),
            Case('1--100x2', 1, '1--100x2'),
            Case('1--100x2', 0, '1--100x2'),
            Case('1--100x2', -1, '1--100x2'),
        ]

        for case in tests:
            actual = padFrameRange(case.frange, case.pad)
            self.assertEqual(actual, case.expected, str(case))
            self.assertNativeStr(actual)

    def testFilterByPaddingNum(self):
        class Case(object):
            def __init__(self, paths, pad, expected, has_padded):
                self.paths = paths
                self.pad = pad
                self.expected = expected
                self.has_padded = has_padded
            def __repr__(self):
                return "Case(paths={!r}, pad={}, has_padded={})".format(self.paths, self.pad, self.has_padded)

        tests = [
            Case(['file.1.ext'], 1, ['file.1.ext'], has_padded=False),
            Case(['file.1.ext'], 2, [], has_padded=False),
            Case(['file.100.ext', 'file.002.ext'], 3, ['file.100.ext', 'file.002.ext'], has_padded=True),
            Case(['file.100.ext', 'file.002.ext'], 1, ['file.100.ext'], has_padded=False),
        ]

        for test in tests:
            filter_ctx = FileSequence._FilterByPaddingNum()
            actual = list(filter_ctx(test.paths, test.pad))
            self.assertEqual(test.expected, actual)
            self.assertEqual(test.has_padded, filter_ctx.has_padded_frames)


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