# -*- coding: utf-8 -*-
import errno
import sys
import unittest
import tempfile
import shutil
import os
import syslog

from supervisor.compat import PY2
from supervisor.compat import as_string
from supervisor.compat import StringIO
from supervisor.compat import unicode

from supervisor.tests.base import mock
from supervisor.tests.base import DummyStream

class LevelTests(unittest.TestCase):
    def test_LOG_LEVELS_BY_NUM_doesnt_include_builtins(self):
        from supervisor import loggers
        for level_name in loggers.LOG_LEVELS_BY_NUM.values():
            self.assertFalse(level_name.startswith('_'))

class HandlerTests:
    def setUp(self):
        self.basedir = tempfile.mkdtemp()
        self.filename = os.path.join(self.basedir, 'thelog')

    def tearDown(self):
        try:
            shutil.rmtree(self.basedir)
        except OSError:
            pass

    def _makeOne(self, *arg, **kw):
        klass = self._getTargetClass()
        return klass(*arg, **kw)

    def _makeLogRecord(self, msg):
        from supervisor import loggers
        record = loggers.LogRecord(
            level=loggers.LevelsByName.INFO,
            msg=msg,
            exc_info=None
            )
        return record

class BareHandlerTests(HandlerTests, unittest.TestCase):
    def _getTargetClass(self):
        from supervisor.loggers import Handler
        return Handler

    def test_flush_stream_flush_raises_IOError_EPIPE(self):
        stream = DummyStream(error=IOError(errno.EPIPE))
        inst = self._makeOne(stream=stream)
        self.assertEqual(inst.flush(), None) # does not raise

    def test_flush_stream_flush_raises_IOError_not_EPIPE(self):
        stream = DummyStream(error=IOError(errno.EALREADY))
        inst = self._makeOne(stream=stream)
        self.assertRaises(IOError, inst.flush) # non-EPIPE IOError raises

    def test_close_already_closed(self):
        stream = DummyStream()
        inst = self._makeOne(stream=stream)
        inst.closed = True
        self.assertEqual(inst.close(), None)

    def test_close_stream_fileno_above_3(self):
        stream = DummyStream(fileno=50)
        inst = self._makeOne(stream=stream)
        self.assertEqual(inst.close(), None)
        self.assertTrue(inst.closed)
        self.assertTrue(inst.stream.closed)

    def test_close_stream_fileno_below_3(self):
        stream = DummyStream(fileno=0)
        inst = self._makeOne(stream=stream)
        self.assertEqual(inst.close(), None)
        self.assertFalse(inst.closed)
        self.assertFalse(inst.stream.closed)

    def test_close_stream_handles_fileno_unsupported_operation(self):
        # on python 2, StringIO does not have fileno()
        # on python 3, StringIO has fileno() but calling it raises
        stream = StringIO()
        inst = self._makeOne(stream=stream)
        inst.close() # shouldn't raise
        self.assertTrue(inst.closed)

    def test_close_stream_handles_fileno_ioerror(self):
        stream = DummyStream()
        def raise_ioerror():
            raise IOError()
        stream.fileno = raise_ioerror
        inst = self._makeOne(stream=stream)
        inst.close() # shouldn't raise
        self.assertTrue(inst.closed)

    def test_emit_gardenpath(self):
        stream = DummyStream()
        inst = self._makeOne(stream=stream)
        record = self._makeLogRecord(b'foo')
        inst.emit(record)
        self.assertEqual(stream.flushed, True)
        self.assertEqual(stream.written, b'foo')

    def test_emit_unicode_error(self):
        stream = DummyStream(error=UnicodeError)
        inst = self._makeOne(stream=stream)
        record = self._makeLogRecord(b'foo')
        inst.emit(record)
        self.assertEqual(stream.flushed, True)
        self.assertEqual(stream.written, b'foo')

    def test_emit_other_error(self):
        stream = DummyStream(error=ValueError)
        inst = self._makeOne(stream=stream)
        handled = []
        inst.handleError = lambda: handled.append(True)
        record = self._makeLogRecord(b'foo')
        inst.emit(record)
        self.assertEqual(stream.flushed, False)
        self.assertEqual(stream.written, b'')

class FileHandlerTests(HandlerTests, unittest.TestCase):
    def _getTargetClass(self):
        from supervisor.loggers import FileHandler
        return FileHandler

    def test_ctor(self):
        handler = self._makeOne(self.filename)
        self.assertTrue(os.path.exists(self.filename), self.filename)
        self.assertEqual(handler.mode, 'ab')
        self.assertEqual(handler.baseFilename, self.filename)
        self.assertEqual(handler.stream.name, self.filename)
        handler.close()

    def test_close(self):
        handler = self._makeOne(self.filename)
        handler.stream.close()
        handler.stream = DummyStream()
        handler.close()
        self.assertEqual(handler.stream.closed, True)

    def test_close_raises(self):
        handler = self._makeOne(self.filename)
        handler.stream.close()
        handler.stream = DummyStream(OSError)
        self.assertRaises(OSError, handler.close)
        self.assertEqual(handler.stream.closed, False)

    def test_reopen(self):
        handler = self._makeOne(self.filename)
        handler.stream.close()
        stream = DummyStream()
        handler.stream = stream
        handler.reopen()
        self.assertEqual(stream.closed, True)
        self.assertEqual(handler.stream.name, self.filename)
        handler.close()

    def test_reopen_raises(self):
        handler = self._makeOne(self.filename)
        handler.stream.close()
        stream = DummyStream()
        handler.stream = stream
        handler.baseFilename = os.path.join(self.basedir, 'notthere', 'a.log')
        self.assertRaises(IOError, handler.reopen)
        self.assertEqual(stream.closed, True)

    def test_remove_exists(self):
        handler = self._makeOne(self.filename)
        self.assertTrue(os.path.exists(self.filename), self.filename)
        handler.remove()
        self.assertFalse(os.path.exists(self.filename), self.filename)

    def test_remove_doesntexist(self):
        handler = self._makeOne(self.filename)
        os.remove(self.filename)
        self.assertFalse(os.path.exists(self.filename), self.filename)
        handler.remove() # should not raise
        self.assertFalse(os.path.exists(self.filename), self.filename)

    def test_remove_raises(self):
        handler = self._makeOne(self.filename)
        os.remove(self.filename)
        os.mkdir(self.filename)
        self.assertTrue(os.path.exists(self.filename), self.filename)
        self.assertRaises(OSError, handler.remove)

    def test_emit_ascii_noerror(self):
        handler = self._makeOne(self.filename)
        record = self._makeLogRecord(b'hello!')
        handler.emit(record)
        handler.close()
        with open(self.filename, 'rb') as f:
            self.assertEqual(f.read(), b'hello!')

    def test_emit_unicode_noerror(self):
        handler = self._makeOne(self.filename)
        record = self._makeLogRecord(b'fi\xc3\xad')
        handler.emit(record)
        handler.close()
        with open(self.filename, 'rb') as f:
            self.assertEqual(f.read(), b'fi\xc3\xad')

    def test_emit_error(self):
        handler = self._makeOne(self.filename)
        handler.stream.close()
        handler.stream = DummyStream(error=OSError)
        record = self._makeLogRecord(b'hello!')
        try:
            old_stderr = sys.stderr
            dummy_stderr = DummyStream()
            sys.stderr = dummy_stderr
            handler.emit(record)
        finally:
            sys.stderr = old_stderr

        self.assertTrue(dummy_stderr.written.endswith(b'OSError\n'),
                        dummy_stderr.written)

if os.path.exists('/dev/stdout'):
    StdoutTestsBase = FileHandlerTests
else:
    # Skip the stdout tests on platforms that don't have /dev/stdout.
    StdoutTestsBase = object

class StdoutTests(StdoutTestsBase):
    def test_ctor_with_dev_stdout(self):
        handler = self._makeOne('/dev/stdout')
        # Modes 'w' and 'a' have the same semantics when applied to
        # character device files and fifos.
        self.assertTrue(handler.mode in ['wb', 'ab'], handler.mode)
        self.assertEqual(handler.baseFilename, '/dev/stdout')
        self.assertEqual(handler.stream.name, '/dev/stdout')
        handler.close()

class RotatingFileHandlerTests(FileHandlerTests):

    def _getTargetClass(self):
        from supervisor.loggers import RotatingFileHandler
        return RotatingFileHandler

    def test_ctor(self):
        handler = self._makeOne(self.filename)
        self.assertEqual(handler.mode, 'ab')
        self.assertEqual(handler.maxBytes, 512*1024*1024)
        self.assertEqual(handler.backupCount, 10)
        handler.close()

    def test_emit_does_rollover(self):
        handler = self._makeOne(self.filename, maxBytes=10, backupCount=2)
        record = self._makeLogRecord(b'a' * 4)

        handler.emit(record) # 4 bytes
        self.assertFalse(os.path.exists(self.filename + '.1'))
        self.assertFalse(os.path.exists(self.filename + '.2'))

        handler.emit(record) # 8 bytes
        self.assertFalse(os.path.exists(self.filename + '.1'))
        self.assertFalse(os.path.exists(self.filename + '.2'))

        handler.emit(record) # 12 bytes, do rollover
        self.assertTrue(os.path.exists(self.filename + '.1'))
        self.assertFalse(os.path.exists(self.filename + '.2'))

        handler.emit(record) # 16 bytes
        self.assertTrue(os.path.exists(self.filename + '.1'))
        self.assertFalse(os.path.exists(self.filename + '.2'))

        handler.emit(record) # 20 bytes
        self.assertTrue(os.path.exists(self.filename + '.1'))
        self.assertFalse(os.path.exists(self.filename + '.2'))

        handler.emit(record) # 24 bytes, do rollover
        self.assertTrue(os.path.exists(self.filename + '.1'))
        self.assertTrue(os.path.exists(self.filename + '.2'))

        handler.emit(record) # 28 bytes
        handler.close()
        self.assertTrue(os.path.exists(self.filename + '.1'))
        self.assertTrue(os.path.exists(self.filename + '.2'))

        with open(self.filename, 'rb') as f:
            self.assertEqual(f.read(), b'a' * 4)

        with open(self.filename+'.1', 'rb') as f:
            self.assertEqual(f.read(), b'a' * 12)

        with open(self.filename+'.2', 'rb') as f:
            self.assertEqual(f.read(), b'a' * 12)

    def test_current_logfile_removed(self):
        handler = self._makeOne(self.filename, maxBytes=6, backupCount=1)
        record = self._makeLogRecord(b'a' * 4)

        handler.emit(record) # 4 bytes
        self.assertTrue(os.path.exists(self.filename))
        self.assertFalse(os.path.exists(self.filename + '.1'))

        # Someone removes the active log file! :-(
        os.unlink(self.filename)
        self.assertFalse(os.path.exists(self.filename))

        handler.emit(record) # 8 bytes, do rollover
        handler.close()
        self.assertTrue(os.path.exists(self.filename))
        self.assertFalse(os.path.exists(self.filename + '.1'))

    def test_removeAndRename_destination_does_not_exist(self):
        inst = self._makeOne(self.filename)
        renames = []
        removes = []
        inst._remove = lambda v: removes.append(v)
        inst._exists = lambda v: False
        inst._rename = lambda s, t: renames.append((s, t))
        inst.removeAndRename('foo', 'bar')
        self.assertEqual(renames, [('foo', 'bar')])
        self.assertEqual(removes, [])
        inst.close()

    def test_removeAndRename_destination_exists(self):
        inst = self._makeOne(self.filename)
        renames = []
        removes = []
        inst._remove = lambda v: removes.append(v)
        inst._exists = lambda v: True
        inst._rename = lambda s, t: renames.append((s, t))
        inst.removeAndRename('foo', 'bar')
        self.assertEqual(renames, [('foo', 'bar')])
        self.assertEqual(removes, ['bar'])
        inst.close()

    def test_removeAndRename_remove_raises_ENOENT(self):
        def remove(fn):
            raise OSError(errno.ENOENT)
        inst = self._makeOne(self.filename)
        renames = []
        inst._remove = remove
        inst._exists = lambda v: True
        inst._rename = lambda s, t: renames.append((s, t))
        inst.removeAndRename('foo', 'bar')
        self.assertEqual(renames, [('foo', 'bar')])
        inst.close()

    def test_removeAndRename_remove_raises_other_than_ENOENT(self):
        def remove(fn):
            raise OSError(errno.EAGAIN)
        inst = self._makeOne(self.filename)
        inst._remove = remove
        inst._exists = lambda v: True
        self.assertRaises(OSError, inst.removeAndRename, 'foo', 'bar')
        inst.close()

    def test_removeAndRename_rename_raises_ENOENT(self):
        def rename(s, d):
            raise OSError(errno.ENOENT)
        inst = self._makeOne(self.filename)
        inst._rename = rename
        inst._exists = lambda v: False
        self.assertEqual(inst.removeAndRename('foo', 'bar'), None)
        inst.close()

    def test_removeAndRename_rename_raises_other_than_ENOENT(self):
        def rename(s, d):
            raise OSError(errno.EAGAIN)
        inst = self._makeOne(self.filename)
        inst._rename = rename
        inst._exists = lambda v: False
        self.assertRaises(OSError, inst.removeAndRename, 'foo', 'bar')
        inst.close()

    def test_doRollover_maxbytes_lte_zero(self):
        inst = self._makeOne(self.filename)
        inst.maxBytes = 0
        self.assertEqual(inst.doRollover(), None)
        inst.close()


class BoundIOTests(unittest.TestCase):
    def _getTargetClass(self):
        from supervisor.loggers import BoundIO
        return BoundIO

    def _makeOne(self, maxbytes, buf=''):
        klass = self._getTargetClass()
        return klass(maxbytes, buf)

    def test_write_overflow(self):
        io = self._makeOne(1, b'a')
        io.write(b'b')
        self.assertEqual(io.buf, b'b')

    def test_getvalue(self):
        io = self._makeOne(1, b'a')
        self.assertEqual(io.getvalue(), b'a')

    def test_clear(self):
        io = self._makeOne(1, b'a')
        io.clear()
        self.assertEqual(io.buf, b'')

    def test_close(self):
        io = self._makeOne(1, b'a')
        io.close()
        self.assertEqual(io.buf, b'')

class LoggerTests(unittest.TestCase):
    def _getTargetClass(self):
        from supervisor.loggers import Logger
        return Logger

    def _makeOne(self, level=None, handlers=None):
        klass = self._getTargetClass()
        return klass(level, handlers)

    def test_blather(self):
        from supervisor.loggers import LevelsByName
        handler = DummyHandler(LevelsByName.BLAT)
        logger = self._makeOne(LevelsByName.BLAT, (handler,))
        logger.blather('hello')
        self.assertEqual(len(handler.records), 1)
        logger.level = LevelsByName.TRAC
        logger.blather('hello')
        self.assertEqual(len(handler.records), 1)

    def test_trace(self):
        from supervisor.loggers import LevelsByName
        handler = DummyHandler(LevelsByName.TRAC)
        logger = self._makeOne(LevelsByName.TRAC, (handler,))
        logger.trace('hello')
        self.assertEqual(len(handler.records), 1)
        logger.level = LevelsByName.DEBG
        logger.trace('hello')
        self.assertEqual(len(handler.records), 1)

    def test_debug(self):
        from supervisor.loggers import LevelsByName
        handler = DummyHandler(LevelsByName.DEBG)
        logger = self._makeOne(LevelsByName.DEBG, (handler,))
        logger.debug('hello')
        self.assertEqual(len(handler.records), 1)
        logger.level = LevelsByName.INFO
        logger.debug('hello')
        self.assertEqual(len(handler.records), 1)

    def test_info(self):
        from supervisor.loggers import LevelsByName
        handler = DummyHandler(LevelsByName.INFO)
        logger = self._makeOne(LevelsByName.INFO, (handler,))
        logger.info('hello')
        self.assertEqual(len(handler.records), 1)
        logger.level = LevelsByName.WARN
        logger.info('hello')
        self.assertEqual(len(handler.records), 1)

    def test_warn(self):
        from supervisor.loggers import LevelsByName
        handler = DummyHandler(LevelsByName.WARN)
        logger = self._makeOne(LevelsByName.WARN, (handler,))
        logger.warn('hello')
        self.assertEqual(len(handler.records), 1)
        logger.level = LevelsByName.ERRO
        logger.warn('hello')
        self.assertEqual(len(handler.records), 1)

    def test_error(self):
        from supervisor.loggers import LevelsByName
        handler = DummyHandler(LevelsByName.ERRO)
        logger = self._makeOne(LevelsByName.ERRO, (handler,))
        logger.error('hello')
        self.assertEqual(len(handler.records), 1)
        logger.level = LevelsByName.CRIT
        logger.error('hello')
        self.assertEqual(len(handler.records), 1)

    def test_critical(self):
        from supervisor.loggers import LevelsByName
        handler = DummyHandler(LevelsByName.CRIT)
        logger = self._makeOne(LevelsByName.CRIT, (handler,))
        logger.critical('hello')
        self.assertEqual(len(handler.records), 1)

    def test_close(self):
        from supervisor.loggers import LevelsByName
        handler = DummyHandler(LevelsByName.CRIT)
        logger = self._makeOne(LevelsByName.CRIT, (handler,))
        logger.close()
        self.assertEqual(handler.closed, True)

    def test_getvalue(self):
        from supervisor.loggers import LevelsByName
        handler = DummyHandler(LevelsByName.CRIT)
        logger = self._makeOne(LevelsByName.CRIT, (handler,))
        self.assertRaises(NotImplementedError, logger.getvalue)


class MockSysLog(mock.Mock):
    def __call__(self, *args, **kwargs):
        message = args[-1]
        if sys.version_info < (3, 0) and isinstance(message, unicode):
            # Python 2.x raises a UnicodeEncodeError when attempting to
            #  transmit unicode characters that don't encode in the
            #  default encoding.
            message.encode()
        super(MockSysLog, self).__call__(*args, **kwargs)

class SyslogHandlerTests(HandlerTests, unittest.TestCase):
    def setUp(self):
        pass

    def tearDown(self):
        pass

    def _getTargetClass(self):
        return __import__('supervisor.loggers').loggers.SyslogHandler

    def _makeOne(self):
        return self._getTargetClass()()

    def test_emit_record_asdict_raises(self):
        class Record(object):
            def asdict(self):
                raise TypeError
        record = Record()
        handler = self._makeOne()
        handled = []
        handler.handleError = lambda: handled.append(True)
        handler.emit(record)
        self.assertEqual(handled, [True])


    @mock.patch('syslog.syslog', MockSysLog())
    def test_emit_ascii_noerror(self):
        handler = self._makeOne()
        record = self._makeLogRecord(b'hello!')
        handler.emit(record)
        syslog.syslog.assert_called_with('hello!')
        record = self._makeLogRecord('hi!')
        handler.emit(record)
        syslog.syslog.assert_called_with('hi!')

    @mock.patch('syslog.syslog', MockSysLog())
    def test_close(self):
        handler = self._makeOne()
        handler.close()  # no-op for syslog

    @mock.patch('syslog.syslog', MockSysLog())
    def test_reopen(self):
        handler = self._makeOne()
        handler.reopen()  # no-op for syslog

    if PY2:
        @mock.patch('syslog.syslog', MockSysLog())
        def test_emit_unicode_noerror(self):
            handler = self._makeOne()
            inp = as_string('fií')
            record = self._makeLogRecord(inp)
            handler.emit(record)
            syslog.syslog.assert_called_with('fi\xc3\xad')
        def test_emit_unicode_witherror(self):
            handler = self._makeOne()
            called = []
            def fake_syslog(msg):
                if not called:
                    called.append(msg)
                    raise UnicodeError
            handler._syslog = fake_syslog
            record = self._makeLogRecord(as_string('fií'))
            handler.emit(record)
            self.assertEqual(called, [as_string('fi\xc3\xad')])
    else:
        @mock.patch('syslog.syslog', MockSysLog())
        def test_emit_unicode_noerror(self):
            handler = self._makeOne()
            record = self._makeLogRecord('fií')
            handler.emit(record)
            syslog.syslog.assert_called_with('fií')
        def test_emit_unicode_witherror(self):
            handler = self._makeOne()
            called = []
            def fake_syslog(msg):
                if not called:
                    called.append(msg)
                    raise UnicodeError
            handler._syslog = fake_syslog
            record = self._makeLogRecord('fií')
            handler.emit(record)
            self.assertEqual(called, ['fií'])

class DummyHandler:
    close = False
    def __init__(self, level):
        self.level = level
        self.records = []
    def emit(self, record):
        self.records.append(record)
    def close(self):
        self.closed = True
