#!/usr/bin/env python

import unittest
from unittest import mock

from mkdocs.commands import build
from mkdocs.exceptions import PluginError
from mkdocs.structure.files import File, Files
from mkdocs.structure.nav import get_navigation
from mkdocs.structure.pages import Page
from mkdocs.tests.base import PathAssertionMixin, load_config, tempdir
from mkdocs.utils import meta


def build_page(title, path, config, md_src=''):
    """Helper which returns a Page object."""

    files = Files([File(path, config.docs_dir, config.site_dir, config.use_directory_urls)])
    page = Page(title, list(files)[0], config)
    # Fake page.read_source()
    page.markdown, page.meta = meta.get_data(md_src)
    return page, files


class BuildTests(PathAssertionMixin, unittest.TestCase):
    def _get_env_with_null_translations(self, config):
        env = config.theme.get_env()
        env.add_extension('jinja2.ext.i18n')
        env.install_null_translations()
        return env

    # Test build.get_context

    def test_context_base_url_homepage(self):
        nav_cfg = [
            {'Home': 'index.md'},
        ]
        cfg = load_config(nav=nav_cfg, use_directory_urls=False)
        fs = [
            File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
        ]
        files = Files(fs)
        nav = get_navigation(files, cfg)
        context = build.get_context(nav, files, cfg, nav.pages[0])
        self.assertEqual(context['base_url'], '.')

    def test_context_base_url_homepage_use_directory_urls(self):
        nav_cfg = [
            {'Home': 'index.md'},
        ]
        cfg = load_config(nav=nav_cfg)
        fs = [
            File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
        ]
        files = Files(fs)
        nav = get_navigation(files, cfg)
        context = build.get_context(nav, files, cfg, nav.pages[0])
        self.assertEqual(context['base_url'], '.')

    def test_context_base_url_nested_page(self):
        nav_cfg = [
            {'Home': 'index.md'},
            {'Nested': 'foo/bar.md'},
        ]
        cfg = load_config(nav=nav_cfg, use_directory_urls=False)
        fs = [
            File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
            File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
        ]
        files = Files(fs)
        nav = get_navigation(files, cfg)
        context = build.get_context(nav, files, cfg, nav.pages[1])
        self.assertEqual(context['base_url'], '..')

    def test_context_base_url_nested_page_use_directory_urls(self):
        nav_cfg = [
            {'Home': 'index.md'},
            {'Nested': 'foo/bar.md'},
        ]
        cfg = load_config(nav=nav_cfg)
        fs = [
            File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
            File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
        ]
        files = Files(fs)
        nav = get_navigation(files, cfg)
        context = build.get_context(nav, files, cfg, nav.pages[1])
        self.assertEqual(context['base_url'], '../..')

    def test_context_base_url_relative_no_page(self):
        cfg = load_config(use_directory_urls=False)
        context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='..')
        self.assertEqual(context['base_url'], '..')

    def test_context_base_url_relative_no_page_use_directory_urls(self):
        cfg = load_config()
        context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='..')
        self.assertEqual(context['base_url'], '..')

    def test_context_base_url_absolute_no_page(self):
        cfg = load_config(use_directory_urls=False)
        context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='/')
        self.assertEqual(context['base_url'], '/')

    def test_context_base_url__absolute_no_page_use_directory_urls(self):
        cfg = load_config()
        context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='/')
        self.assertEqual(context['base_url'], '/')

    def test_context_base_url_absolute_nested_no_page(self):
        cfg = load_config(use_directory_urls=False)
        context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='/foo/')
        self.assertEqual(context['base_url'], '/foo/')

    def test_context_base_url__absolute_nested_no_page_use_directory_urls(self):
        cfg = load_config()
        context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='/foo/')
        self.assertEqual(context['base_url'], '/foo/')

    def test_context_extra_css_js_from_homepage(self):
        nav_cfg = [
            {'Home': 'index.md'},
        ]
        cfg = load_config(
            nav=nav_cfg,
            extra_css=['style.css'],
            extra_javascript=['script.js'],
            use_directory_urls=False,
        )
        fs = [
            File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
        ]
        files = Files(fs)
        nav = get_navigation(files, cfg)
        context = build.get_context(nav, files, cfg, nav.pages[0])
        self.assertEqual(context['extra_css'], ['style.css'])
        self.assertEqual(context['extra_javascript'], ['script.js'])

    def test_context_extra_css_js_from_nested_page(self):
        nav_cfg = [
            {'Home': 'index.md'},
            {'Nested': 'foo/bar.md'},
        ]
        cfg = load_config(
            nav=nav_cfg,
            extra_css=['style.css'],
            extra_javascript=['script.js'],
            use_directory_urls=False,
        )
        fs = [
            File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
            File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
        ]
        files = Files(fs)
        nav = get_navigation(files, cfg)
        context = build.get_context(nav, files, cfg, nav.pages[1])
        self.assertEqual(context['extra_css'], ['../style.css'])
        self.assertEqual(context['extra_javascript'], ['../script.js'])

    def test_context_extra_css_js_from_nested_page_use_directory_urls(self):
        nav_cfg = [
            {'Home': 'index.md'},
            {'Nested': 'foo/bar.md'},
        ]
        cfg = load_config(
            nav=nav_cfg,
            extra_css=['style.css'],
            extra_javascript=['script.js'],
        )
        fs = [
            File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
            File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
        ]
        files = Files(fs)
        nav = get_navigation(files, cfg)
        context = build.get_context(nav, files, cfg, nav.pages[1])
        self.assertEqual(context['extra_css'], ['../../style.css'])
        self.assertEqual(context['extra_javascript'], ['../../script.js'])

    # TODO: This shouldn't pass on Linux
    # @unittest.skipUnless(sys.platform.startswith("win"), "requires Windows")
    def test_context_extra_css_path_warning(self):
        nav_cfg = [
            {'Home': 'index.md'},
        ]
        cfg = load_config(
            nav=nav_cfg,
            extra_css=['assets\\style.css'],
            use_directory_urls=False,
        )
        fs = [
            File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
        ]
        files = Files(fs)
        nav = get_navigation(files, cfg)
        with self.assertLogs('mkdocs') as cm:
            context = build.get_context(nav, files, cfg, nav.pages[0])
        self.assertEqual(context['extra_css'], ['assets/style.css'])
        self.assertEqual(
            '\n'.join(cm.output),
            "WARNING:mkdocs.utils:Path 'assets\\style.css' uses OS-specific separator '\\'. "
            "That will be unsupported in a future release. Please change it to '/'.",
        )

    def test_context_extra_css_js_no_page(self):
        cfg = load_config(extra_css=['style.css'], extra_javascript=['script.js'])
        context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='..')
        self.assertEqual(context['extra_css'], ['../style.css'])
        self.assertEqual(context['extra_javascript'], ['../script.js'])

    def test_extra_context(self):
        cfg = load_config(extra={'a': 1})
        context = build.get_context(mock.Mock(), mock.Mock(), cfg)
        self.assertEqual(context['config']['extra']['a'], 1)

    # Test build._build_theme_template

    @mock.patch('mkdocs.utils.write_file')
    @mock.patch('mkdocs.commands.build._build_template', return_value='some content')
    def test_build_theme_template(self, mock_build_template, mock_write_file):
        cfg = load_config()
        env = cfg['theme'].get_env()
        build._build_theme_template('main.html', env, mock.Mock(), cfg, mock.Mock())
        mock_write_file.assert_called_once()
        mock_build_template.assert_called_once()

    @mock.patch('mkdocs.utils.write_file')
    @mock.patch('mkdocs.commands.build._build_template', return_value='some content')
    @mock.patch('gzip.GzipFile')
    @tempdir()
    def test_build_sitemap_template(
        self, site_dir, mock_gzip_gzipfile, mock_build_template, mock_write_file
    ):
        cfg = load_config(site_dir=site_dir)
        env = cfg['theme'].get_env()
        build._build_theme_template('sitemap.xml', env, mock.Mock(), cfg, mock.Mock())
        mock_write_file.assert_called_once()
        mock_build_template.assert_called_once()
        mock_gzip_gzipfile.assert_called_once()

    @mock.patch('mkdocs.utils.write_file')
    @mock.patch('mkdocs.commands.build._build_template', return_value='')
    def test_skip_missing_theme_template(self, mock_build_template, mock_write_file):
        cfg = load_config()
        env = cfg['theme'].get_env()
        with self.assertLogs('mkdocs') as cm:
            build._build_theme_template('missing.html', env, mock.Mock(), cfg, mock.Mock())
        self.assertEqual(
            '\n'.join(cm.output),
            "WARNING:mkdocs.commands.build:Template skipped: 'missing.html' not found in theme directories.",
        )
        mock_write_file.assert_not_called()
        mock_build_template.assert_not_called()

    @mock.patch('mkdocs.utils.write_file')
    @mock.patch('mkdocs.commands.build._build_template', return_value='')
    def test_skip_theme_template_empty_output(self, mock_build_template, mock_write_file):
        cfg = load_config()
        env = cfg['theme'].get_env()
        with self.assertLogs('mkdocs') as cm:
            build._build_theme_template('main.html', env, mock.Mock(), cfg, mock.Mock())
        self.assertEqual(
            '\n'.join(cm.output),
            "INFO:mkdocs.commands.build:Template skipped: 'main.html' generated empty output.",
        )
        mock_write_file.assert_not_called()
        mock_build_template.assert_called_once()

    # Test build._build_extra_template

    @tempdir()
    @mock.patch('mkdocs.commands.build.open', mock.mock_open(read_data='template content'))
    def test_build_extra_template(self, site_dir):
        cfg = load_config(site_dir=site_dir)
        fs = [
            File('foo.html', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
        ]
        files = Files(fs)
        build._build_extra_template('foo.html', files, cfg, mock.Mock())

    @mock.patch('mkdocs.commands.build.open', mock.mock_open(read_data='template content'))
    def test_skip_missing_extra_template(self):
        cfg = load_config()
        fs = [
            File('foo.html', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
        ]
        files = Files(fs)
        with self.assertLogs('mkdocs') as cm:
            build._build_extra_template('missing.html', files, cfg, mock.Mock())
        self.assertEqual(
            '\n'.join(cm.output),
            "WARNING:mkdocs.commands.build:Template skipped: 'missing.html' not found in docs_dir.",
        )

    @mock.patch('mkdocs.commands.build.open', side_effect=OSError('Error message.'))
    def test_skip_ioerror_extra_template(self, mock_open):
        cfg = load_config()
        fs = [
            File('foo.html', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
        ]
        files = Files(fs)
        with self.assertLogs('mkdocs') as cm:
            build._build_extra_template('foo.html', files, cfg, mock.Mock())
        self.assertEqual(
            '\n'.join(cm.output),
            "WARNING:mkdocs.commands.build:Error reading template 'foo.html': Error message.",
        )

    @mock.patch('mkdocs.commands.build.open', mock.mock_open(read_data=''))
    def test_skip_extra_template_empty_output(self):
        cfg = load_config()
        fs = [
            File('foo.html', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
        ]
        files = Files(fs)
        with self.assertLogs('mkdocs') as cm:
            build._build_extra_template('foo.html', files, cfg, mock.Mock())
        self.assertEqual(
            '\n'.join(cm.output),
            "INFO:mkdocs.commands.build:Template skipped: 'foo.html' generated empty output.",
        )

    # Test build._populate_page

    @tempdir(files={'index.md': 'page content'})
    def test_populate_page(self, docs_dir):
        cfg = load_config(docs_dir=docs_dir)
        file = File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
        page = Page('Foo', file, cfg)
        build._populate_page(page, cfg, Files([file]))
        self.assertEqual(page.content, '<p>page content</p>')

    @tempdir(files={'testing.html': '<p>page content</p>'})
    def test_populate_page_dirty_modified(self, site_dir):
        cfg = load_config(site_dir=site_dir)
        file = File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
        page = Page('Foo', file, cfg)
        build._populate_page(page, cfg, Files([file]), dirty=True)
        self.assertTrue(page.markdown.startswith('# Welcome to MkDocs'))
        self.assertTrue(
            page.content.startswith('<h1 id="welcome-to-mkdocs">Welcome to MkDocs</h1>')
        )

    @tempdir(files={'index.md': 'page content'})
    @tempdir(files={'index.html': '<p>page content</p>'})
    def test_populate_page_dirty_not_modified(self, site_dir, docs_dir):
        cfg = load_config(docs_dir=docs_dir, site_dir=site_dir)
        file = File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
        page = Page('Foo', file, cfg)
        build._populate_page(page, cfg, Files([file]), dirty=True)
        # Content is empty as file read was skipped
        self.assertEqual(page.markdown, None)
        self.assertEqual(page.content, None)

    @tempdir(files={'index.md': 'new page content'})
    @mock.patch('mkdocs.structure.pages.open', side_effect=OSError('Error message.'))
    def test_populate_page_read_error(self, docs_dir, mock_open):
        cfg = load_config(docs_dir=docs_dir)
        file = File('missing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
        page = Page('Foo', file, cfg)
        with self.assertLogs('mkdocs') as cm:
            with self.assertRaises(OSError):
                build._populate_page(page, cfg, Files([file]))
        self.assertEqual(
            cm.output,
            [
                'ERROR:mkdocs.structure.pages:File not found: missing.md',
                "ERROR:mkdocs.commands.build:Error reading page 'missing.md': Error message.",
            ],
        )
        mock_open.assert_called_once()

    @tempdir(files={'index.md': 'page content'})
    @mock.patch(
        'mkdocs.plugins.PluginCollection.run_event', side_effect=PluginError('Error message.')
    )
    def test_populate_page_read_plugin_error(self, docs_dir, mock_open):
        cfg = load_config(docs_dir=docs_dir)
        file = File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
        page = Page('Foo', file, cfg)
        with self.assertLogs('mkdocs') as cm:
            with self.assertRaises(PluginError):
                build._populate_page(page, cfg, Files([file]))
        self.assertEqual(
            '\n'.join(cm.output),
            "ERROR:mkdocs.commands.build:Error reading page 'index.md':",
        )
        mock_open.assert_called_once()

    # Test build._build_page

    @tempdir()
    def test_build_page(self, site_dir):
        cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[])
        fs = [File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])]
        files = Files(fs)
        nav = get_navigation(files, cfg)
        page = files.documentation_pages()[0].page
        # Fake populate page
        page.title = 'Title'
        page.markdown = 'page content'
        page.content = '<p>page content</p>'
        build._build_page(page, cfg, files, nav, self._get_env_with_null_translations(cfg))
        self.assertPathIsFile(site_dir, 'index.html')

    @tempdir()
    @mock.patch('jinja2.environment.Template.render', return_value='')
    def test_build_page_empty(self, site_dir, render_mock):
        cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[])
        fs = [File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])]
        files = Files(fs)
        nav = get_navigation(files, cfg)
        with self.assertLogs('mkdocs') as cm:
            build._build_page(
                files.documentation_pages()[0].page, cfg, files, nav, cfg['theme'].get_env()
            )
        self.assertEqual(
            '\n'.join(cm.output),
            "INFO:mkdocs.commands.build:Page skipped: 'index.md'. Generated empty output.",
        )
        self.assertPathNotFile(site_dir, 'index.html')
        render_mock.assert_called_once()

    @tempdir(files={'index.md': 'page content'})
    @tempdir(files={'index.html': '<p>page content</p>'})
    @mock.patch('mkdocs.utils.write_file')
    def test_build_page_dirty_modified(self, site_dir, docs_dir, mock_write_file):
        cfg = load_config(docs_dir=docs_dir, site_dir=site_dir, nav=['index.md'], plugins=[])
        fs = [File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])]
        files = Files(fs)
        nav = get_navigation(files, cfg)
        page = files.documentation_pages()[0].page
        # Fake populate page
        page.title = 'Title'
        page.markdown = 'new page content'
        page.content = '<p>new page content</p>'
        build._build_page(
            page, cfg, files, nav, self._get_env_with_null_translations(cfg), dirty=True
        )
        mock_write_file.assert_not_called()

    @tempdir(files={'testing.html': '<p>page content</p>'})
    @mock.patch('mkdocs.utils.write_file')
    def test_build_page_dirty_not_modified(self, site_dir, mock_write_file):
        cfg = load_config(site_dir=site_dir, nav=['testing.md'], plugins=[])
        fs = [File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])]
        files = Files(fs)
        nav = get_navigation(files, cfg)
        page = files.documentation_pages()[0].page
        # Fake populate page
        page.title = 'Title'
        page.markdown = 'page content'
        page.content = '<p>page content</p>'
        build._build_page(
            page, cfg, files, nav, self._get_env_with_null_translations(cfg), dirty=True
        )
        mock_write_file.assert_called_once()

    @tempdir()
    def test_build_page_custom_template(self, site_dir):
        cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[])
        fs = [File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])]
        files = Files(fs)
        nav = get_navigation(files, cfg)
        page = files.documentation_pages()[0].page
        # Fake populate page
        page.title = 'Title'
        page.meta = {'template': '404.html'}
        page.markdown = 'page content'
        page.content = '<p>page content</p>'
        build._build_page(page, cfg, files, nav, self._get_env_with_null_translations(cfg))
        self.assertPathIsFile(site_dir, 'index.html')

    @tempdir()
    @mock.patch('mkdocs.utils.write_file', side_effect=OSError('Error message.'))
    def test_build_page_error(self, site_dir, mock_write_file):
        cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[])
        fs = [File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])]
        files = Files(fs)
        nav = get_navigation(files, cfg)
        page = files.documentation_pages()[0].page
        # Fake populate page
        page.title = 'Title'
        page.markdown = 'page content'
        page.content = '<p>page content</p>'
        with self.assertLogs('mkdocs') as cm:
            with self.assertRaises(OSError):
                build._build_page(page, cfg, files, nav, self._get_env_with_null_translations(cfg))
        self.assertEqual(
            '\n'.join(cm.output),
            "ERROR:mkdocs.commands.build:Error building page 'index.md': Error message.",
        )
        mock_write_file.assert_called_once()

    @tempdir()
    @mock.patch(
        'mkdocs.plugins.PluginCollection.run_event', side_effect=PluginError('Error message.')
    )
    def test_build_page_plugin_error(self, site_dir, mock_write_file):
        cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[])
        fs = [File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])]
        files = Files(fs)
        nav = get_navigation(files, cfg)
        page = files.documentation_pages()[0].page
        # Fake populate page
        page.title = 'Title'
        page.markdown = 'page content'
        page.content = '<p>page content</p>'
        with self.assertLogs('mkdocs') as cm:
            with self.assertRaises(PluginError):
                build._build_page(page, cfg, files, nav, cfg['theme'].get_env())
        self.assertEqual(
            '\n'.join(cm.output),
            "ERROR:mkdocs.commands.build:Error building page 'index.md':",
        )
        mock_write_file.assert_called_once()

    # Test build.build

    @tempdir(
        files={
            'index.md': 'page content',
            'empty.md': '',
            'img.jpg': '',
            'static.html': 'content',
            '.hidden': 'content',
            '.git/hidden': 'content',
        }
    )
    @tempdir()
    def test_copying_media(self, site_dir, docs_dir):
        cfg = load_config(docs_dir=docs_dir, site_dir=site_dir)
        build.build(cfg)

        # Verify that only non-empty md file (converted to html), static HTML file and image are copied.
        self.assertPathIsFile(site_dir, 'index.html')
        self.assertPathIsFile(site_dir, 'img.jpg')
        self.assertPathIsFile(site_dir, 'static.html')
        self.assertPathNotExists(site_dir, 'empty.md')
        self.assertPathNotExists(site_dir, '.hidden')
        self.assertPathNotExists(site_dir, '.git/hidden')

    @tempdir(files={'index.md': 'page content'})
    @tempdir()
    def test_copy_theme_files(self, site_dir, docs_dir):
        cfg = load_config(docs_dir=docs_dir, site_dir=site_dir)
        build.build(cfg)

        # Verify only theme media are copied, not templates, Python or localization files.
        self.assertPathIsFile(site_dir, 'index.html')
        self.assertPathIsFile(site_dir, '404.html')
        self.assertPathIsDir(site_dir, 'js')
        self.assertPathIsDir(site_dir, 'css')
        self.assertPathIsDir(site_dir, 'img')
        self.assertPathNotExists(site_dir, '__init__.py')
        self.assertPathNotExists(site_dir, '__init__.pyc')
        self.assertPathNotExists(site_dir, 'base.html')
        self.assertPathNotExists(site_dir, 'content.html')
        self.assertPathNotExists(site_dir, 'main.html')
        self.assertPathNotExists(site_dir, 'locales')

    # Test build.site_directory_contains_stale_files

    @tempdir(files=['index.html'])
    def test_site_dir_contains_stale_files(self, site_dir):
        self.assertTrue(build.site_directory_contains_stale_files(site_dir))

    @tempdir()
    def test_not_site_dir_contains_stale_files(self, site_dir):
        self.assertFalse(build.site_directory_contains_stale_files(site_dir))
