class SuitePageServerTest(TempWorkspaceTestCase):

    NUM_SUITE_DESC = 2

    def setUp(self):

        # Call the superclass implementation to create the temp workspace
        super(SuitePageServerTest, self).setUp()

        # Create mock suite descriptions
        self.suite_desc_list = [
            mock.MagicMock(SuiteDescription)
            for _ in range(self.NUM_SUITE_DESC)
        ]

        # Configure the mock suite descriptions to have no dependencies
        suite_num = 0
        for suite in self.suite_desc_list:
            suite.suite_name.return_value = 'test-suite-{}'.format(suite_num)
            suite.lib_paths.return_value = []
            suite.src_paths.return_value = []
            suite.spec_paths.return_value = []
            suite.fixture_paths.return_value = []
            suite.root_dir.return_value = os.getcwd()
            suite_num += 1

        # Create a mock suite renderer
        self.suite_renderer = mock.MagicMock(SuiteRenderer)

        # Create the server
        self.server = SuitePageServer(
            self.suite_desc_list, self.suite_renderer
        )

        # Start the server
        self.server.start()

    def tearDown(self):

        # Stop the server, which frees the port
        self.server.stop()

    def test_root_url(self):

        # Check that the root URL has the right form
        url_regex = re.compile('^http://0.0.0.0:[0-9]+/$')
        url = self.server.root_url()
        result = url_regex.match(url)
        self.assertIsNot(result, None,
                         msg="URL has incorrect format: '{}'".format(url))

    def test_suite_url_list(self):

        # Retrieve the urls for each test suite page
        url_list = self.server.suite_url_list()

        # Expect that we have the correct number of URLs
        self.assertEqual(len(url_list), self.NUM_SUITE_DESC)

        # Expect that the URLs have the correct form
        for suite_num in range(self.NUM_SUITE_DESC):
            expected_url = self.server.root_url() + u'suite/test-suite-{}'.format(suite_num)
            self.assertIn(expected_url, url_list)

    def test_enforce_unique_suite_names(self):

        # Try to create a suite server in which two suites have the same name
        suite_desc_list = [
            mock.MagicMock(SuiteDescription)
            for _ in range(4)
        ]

        suite_desc_list[0].suite_name.return_value = 'test-suite-1'
        suite_desc_list[1].suite_name.return_value = 'test-suite-2'
        suite_desc_list[2].suite_name.return_value = 'test-suite-1'
        suite_desc_list[3].suite_name.return_value = 'test-suite-3'

        # Expect an error when initializing the server
        with self.assertRaises(DuplicateSuiteNameError):
            SuitePageServer(suite_desc_list, self.suite_renderer)

    def test_serve_suite_pages(self):

        # Configure the suite renderer to return a test string
        expected_page = u'test suite mock'
        self.suite_renderer.render_to_string.return_value = expected_page

        # Check that we can load each page in the suite
        for url in self.server.suite_url_list():
            self._assert_page_equals(url, expected_page)

    def test_serve_suite_pages_ignore_get_params(self):

        # Configure the suite renderer to return a test string
        expected_page = u'test suite mock'
        self.suite_renderer.render_to_string.return_value = expected_page

        # Check that we can load each page in the suite,
        # even if we add additional GET params
        for url in self.server.suite_url_list():
            url = url + "?param=12345"
            self._assert_page_equals(url, expected_page)

    def test_serve_runners(self):

        for path in ['jasmine/jasmine.css',
                     'jasmine/jasmine.js',
                     'jasmine/jasmine-json.js',
                     'jasmine/jasmine-html.js']:

            pkg_path = 'runner/' + path
            expected_page = pkg_resources.resource_string('js_test_tool', pkg_path)
            url = self.server.root_url() + pkg_path
            self._assert_page_equals(url, expected_page)

    def test_ignore_runner_get_params(self):

        for path in ['jasmine/jasmine.css',
                     'jasmine/jasmine.js',
                     'jasmine/jasmine-json.js',
                     'jasmine/jasmine-html.js']:

            pkg_path = 'runner/' + path
            expected_page = pkg_resources.resource_string('js_test_tool', pkg_path)

            # Append GET params to the URL
            url = self.server.root_url() + pkg_path + "?param=abc.123&another=87"

            # Should still be able to load the page
            self._assert_page_equals(url, expected_page)

    def test_serve_lib_js(self):

        # Configure the suite description to contain JS dependencies
        lib_paths = ['lib/1.js', 'lib/subdir/2.js']
        self.suite_desc_list[0].lib_paths.return_value = lib_paths

        # Create fake files to serve
        os.makedirs('lib/subdir')
        expected_page = u'\u0236est \u023Dib file'
        self._create_fake_files(lib_paths, expected_page)

        # Expect that the server sends us the files
        for path in lib_paths:
            url = self.server.root_url() + 'suite/test-suite-0/include/' + path
            self._assert_page_equals(url, expected_page)

    def test_serve_src_js(self):

        # Configure the suite description to contain JS source files
        src_paths = ['src/1.js', 'src/subdir/2.js']
        self.suite_desc_list[0].src_paths.return_value = src_paths

        # Create fake files to serve
        os.makedirs('src/subdir')
        expected_page = u'test \u023Frc file'
        self._create_fake_files(src_paths, expected_page)

        # Expect that the server sends us the files
        for path in src_paths:
            url = self.server.root_url() + 'suite/test-suite-0/include/' + path
            self._assert_page_equals(url, expected_page)

    def test_serve_spec_js(self):

        # Configure the suite description to contain JS spec files
        spec_paths = ['spec/1.js', 'spec/subdir/2.js']
        self.suite_desc_list[0].spec_paths.return_value = spec_paths

        # Create fake files to serve
        os.makedirs('spec/subdir')
        expected_page = u'test spe\u023C file'
        self._create_fake_files(spec_paths, expected_page)

        # Expect that the server sends us the files
        for path in spec_paths:
            url = self.server.root_url() + 'suite/test-suite-0/include/' + path
            self._assert_page_equals(url, expected_page)

    def test_serve_text_fixtures(self):

        # Configure the suite description to contain test fixture files
        fixture_paths = ['fixtures/1.html', 'fixtures/subdir/2.html']
        self.suite_desc_list[0].fixture_paths.return_value = fixture_paths

        # Create fake files to serve
        os.makedirs('fixtures/subdir')
        expected_page = u'test fi\u039Eture'
        self._create_fake_files(fixture_paths, expected_page)

        # Expect that the server sends us the files
        for path in fixture_paths:
            url = self.server.root_url() + 'suite/test-suite-0/include/' + path
            self._assert_page_equals(url, expected_page)

    def test_serve_binary_fixtures(self):

        # Configure the suite description to contain binary fixture files
        fixture_paths = ['fixtures/test.mp4', 'fixtures/test.png']
        self.suite_desc_list[0].fixture_paths.return_value = fixture_paths

        # Create fake files to serve
        os.mkdir('fixtures')
        file_contents = '\x02\x03\x04\x05\x06'
        self._create_fake_files(fixture_paths, file_contents, encoding=None)

        # Expect that the server sends us the files as 
        # an un-decoded byte stream
        for path in fixture_paths:
            url = self.server.root_url() + 'suite/test-suite-0/include/' + path
            self._assert_page_equals(url, file_contents, encoding=None)

    def test_serve_byte_range_requests(self):

        # Configure the suite description to contain a binary fixture file
        fixture_paths = ['fixtures/test.mp4']
        self.suite_desc_list[0].fixture_paths.return_value = fixture_paths

        # Make this file fairly large, so we can access ranges of it
        file_size = 10000

        # Create a fake file to serve
        os.mkdir('fixtures')
        file_contents = '\x01' * file_size
        self._create_fake_files(fixture_paths, file_contents, encoding=None)

        # Check for byte range support
        url = self.server.root_url() + 'suite/test-suite-0/include/fixtures/test.mp4'
        resp = requests.get(url, headers={'Range': None})

        # Expect that the server supports byte ranges
        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp.headers.get('Accept-Ranges'), 'bytes')

        # Check that we can make requests for byte ranges
        # Examples taken from the RFC:
        # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1
        test_cases = [
            ('0-499', 0, 499),
            ('0-', 0, 9999),
            ('9000-9999', 9000, 9999),
            ('9500-', 9500, 9999),
            ('-500', 9500, 9999),
        ]

        for byte_range, content_start, content_end in test_cases:

            print "Sending byte range '{0}'".format(byte_range)

            resp = requests.get(url, headers={'Range': 'bytes=' + byte_range})

            # Expect that we get a 206 (partial content)
            self.assertEqual(resp.status_code, 206)

            self.assertEqual(
                resp.headers.get('Content-Range'),
                'bytes {0}-{1}/{2}'.format(content_start, content_end, file_size)
            )

            content_len = content_end - content_start + 1
            self.assertEqual(resp.headers.get('Content-Length'), str(content_len))
            self.assertEqual(len(resp.content), content_len)

    def test_serve_multiple_byte_ranges(self):

        # Configure the suite description to contain a binary fixture file
        fixture_paths = ['fixtures/test.mp4']
        self.suite_desc_list[0].fixture_paths.return_value = fixture_paths

        # Create a fake file to serve
        # The file has \x01 as the first byte, \x02 as the middle bytes
        # and \x03 as the last byte
        os.mkdir('fixtures')
        file_contents = '\x01' * 10
        self._create_fake_files(fixture_paths, file_contents, encoding=None)

        # Make a request for multiple byte range
        byte_range = '0-3,4-7'
        url = self.server.root_url() + 'suite/test-suite-0/include/fixtures/test.mp4'
        resp = requests.get(url, headers={'Range': 'bytes=' + byte_range})

        # Expect that the request for multiple ranges is ignored
        # and the whole file is returned
        # (we don't implement this part of the protocol)
        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp.content, file_contents)

    def test_invalid_byte_range(self):

        # Configure the suite description to contain a binary fixture file
        fixture_paths = ['fixtures/test.mp4']
        self.suite_desc_list[0].fixture_paths.return_value = fixture_paths

        # Make this file fairly large, so we can access ranges of it
        file_size = 10000

        # Create a fake file to serve
        os.mkdir('fixtures')
        file_contents = '\x01' * file_size
        self._create_fake_files(fixture_paths, file_contents, encoding=None)

        # Send invalid byte range headers and expect a 200 with the full file returned
        url = self.server.root_url() + 'suite/test-suite-0/include/fixtures/test.mp4'
        for invalid_range in ['not_bytes=0-10', 'bytes = space', 'bytes=text-text', 'bytes=-']:
            resp = requests.get(url, headers={'Range': invalid_range})
            self.assertEqual(resp.status_code, 200)
            self.assertEqual(resp.content, file_contents)

    def test_unsatisfiable_range(self):

        # Configure the suite description to contain a binary fixture file
        fixture_paths = ['fixtures/test.mp4']
        self.suite_desc_list[0].fixture_paths.return_value = fixture_paths

        # Make this file fairly large, so we can access ranges of it
        file_size = 10000

        # Create a fake file to serve
        os.mkdir('fixtures')
        file_contents = '\x01' * file_size
        self._create_fake_files(fixture_paths, file_contents, encoding=None)

        # Send unsatisfiable range (start > end) and expect a 406
        url = self.server.root_url() + 'suite/test-suite-0/include/fixtures/test.mp4'
        resp = requests.get(url, headers={'Range': 'bytes=10-2'})
        self.assertEqual(resp.status_code, 406)

    def test_serve_iso_encoded_dependency(self):

        # Configure the suite description to contain dependency files
        # that are ISO encoded
        dependencies = ['1.js', '2.js', '3.js', '4.js']
        self.suite_desc_list[0].lib_paths.return_value = [dependencies[0]]
        self.suite_desc_list[0].src_paths.return_value = [dependencies[1]]
        self.suite_desc_list[0].spec_paths.return_value = [dependencies[2]]
        self.suite_desc_list[0].fixture_paths.return_value = [dependencies[3]]

        # Create fake files to serve with ISO-8859-1 chars
        page_contents = '\xf6 \x9a \xa0'
        self._create_fake_files(dependencies, page_contents, encoding=None)

        # Expect that the server sends us the files,
        # ignoring any GET parameters we pass in the URL
        expected_page = u'\xf6 \x9a \xa0'
        for path in dependencies:
            url = self.server.root_url() + 'suite/test-suite-0/include/' + path + "?123456"
            self._assert_page_equals(url, expected_page, encoding='iso-8859-1')

    def test_ignore_dependency_get_params(self):

        # Configure the suite description to contain dependency files
        dependencies = ['1.js', '2.js', '3.js', '4.js']
        self.suite_desc_list[0].lib_paths.return_value = [dependencies[0]]
        self.suite_desc_list[0].src_paths.return_value = [dependencies[1]]
        self.suite_desc_list[0].spec_paths.return_value = [dependencies[2]]
        self.suite_desc_list[0].fixture_paths.return_value = [dependencies[3]]

        # Create fake files to serve
        expected_page = u'\u0236est dependency'
        self._create_fake_files(dependencies, expected_page)

        # Expect that the server sends us the files,
        # ignoring any GET parameters we pass in the URL
        for path in dependencies:
            url = self.server.root_url() + 'suite/test-suite-0/include/' + path + "?123456"
            self._assert_page_equals(url, expected_page)

    def test_different_working_dir(self):

        # Configure the suite description to contain JS dependencies
        spec_paths = ['spec/1.js']
        self.suite_desc_list[0].spec_paths.return_value = spec_paths

        # Create fake files to serve
        os.makedirs('spec/subdir')
        expected_page = u'test spec file'
        self._create_fake_files(spec_paths, expected_page)

        # Should be able to change the working directory and still
        # get the dependencies, because the suite description
        # contains the root directory for dependency paths.
        # The superclass `TemplateWorkspaceTestCase` will reset the working
        # directory on `tearDown()`
        os.mkdir('different_dir')
        os.chdir('different_dir')

        # Expect that we still get the files
        for path in spec_paths:
            url = self.server.root_url() + 'suite/test-suite-0/include/' + path
            self._assert_page_equals(url, expected_page)

    def test_404_pages(self):

        # Try a URL that is not one of the suite urls
        root_url = self.server.root_url()
        bad_url_list = [root_url + 'invalid',
                        root_url + 'runner/not_found.txt',
                        root_url + 'suite/{}'.format(self.NUM_SUITE_DESC + 1),
                        root_url + 'suite/{}'.format(-1)]

        # Expect that we get a page not found status
        for bad_url in bad_url_list:
            response = requests.get(bad_url)
            self.assertEqual(response.status_code,
                             requests.codes.not_found,
                             msg=bad_url)

    def test_missing_dependency(self):

        # Configure the suite description to contain a file
        self.suite_desc_list[0].src_paths.return_value = ['not_found.txt']

        # The file does not exist, so expect that we
        # get a not found response
        response = requests.get(self.server.root_url() + 'not_found.txt')
        self.assertEqual(response.status_code, requests.codes.not_found)

    def _assert_page_equals(self, url, expected_content, encoding='utf-8'):
        """
        Assert that the page at `url` contains `expected_content`.
        Uses a GET HTTP request to retrieve the page and expects
        a 200 status code, with UTF-8 encoding.

        `encoding` is the expected encoding.  If None, expect
        an unencoded byte string.
        """

        # HTTP GET request for the page
        response = requests.get(url)

        # Expect that we get a success result code
        self.assertEqual(response.status_code, requests.codes.ok, msg=url)

        # Expect that we got an accurate content length
        if encoding is None:
            expected_len = len(expected_content)
        else:
            expected_len = len(expected_content.encode(encoding))

        self.assertEqual(
            str(expected_len),
            response.headers.get('content-length')
        )

        # Expect that the content is what we rendered
        if encoding is not None:
            self.assertIn(
                expected_content,
                response.content.decode(encoding),
                msg=url
            )

        # If no encoding, just expect the byte string
        else:
            self.assertIn(expected_content, response.content, msg=url)

    @staticmethod
    def _create_fake_files(path_list, contents, encoding='utf8'):
        """
        For each path in `path_list`, create a file containing `contents`
        (a string).
        """

        for path in path_list:
            with open(path, 'w') as fake_file:

                # If an encoding is specified, use it to convert
                # the string to a byte str
                if encoding is not None:
                    encoded_contents = contents.encode(encoding)
                else:
                    encoded_contents = contents

                # Write the byte string to the file
                fake_file.write(encoded_contents)