# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

"""
Test cases for L{twisted.logger._io}.
"""

from __future__ import annotations

import sys

from zope.interface import implementer

from constantly import NamedConstant

from twisted.trial import unittest
from .._interfaces import ILogObserver, LogEvent
from .._io import LoggingFile
from .._levels import LogLevel
from .._logger import Logger
from .._observer import LogPublisher


@implementer(ILogObserver)
class TestLoggingFile(LoggingFile):
    """
    L{LoggingFile} that is also an observer which captures events and messages.
    """

    def __init__(
        self,
        logger: Logger,
        level: NamedConstant = LogLevel.info,
        encoding: str | None = None,
    ) -> None:
        super().__init__(logger=logger, level=level, encoding=encoding)
        self.events: list[LogEvent] = []
        self.messages: list[str] = []

    def __call__(self, event: LogEvent) -> None:
        self.events.append(event)
        if "log_io" in event:
            self.messages.append(event["log_io"])


class LoggingFileTests(unittest.TestCase):
    """
    Tests for L{LoggingFile}.
    """

    def setUp(self) -> None:
        """
        Create a logger for test L{LoggingFile} instances to use.
        """
        self.publisher = LogPublisher()
        self.logger = Logger(observer=self.publisher)

    def test_softspace(self) -> None:
        """
        L{LoggingFile.softspace} is 0.
        """
        self.assertEqual(LoggingFile(self.logger).softspace, 0)

        warningsShown = self.flushWarnings([self.test_softspace])
        self.assertEqual(len(warningsShown), 1)
        self.assertEqual(warningsShown[0]["category"], DeprecationWarning)
        deprecatedClass = "twisted.logger._io.LoggingFile.softspace"
        self.assertEqual(
            warningsShown[0]["message"],
            "%s was deprecated in Twisted 21.2.0" % (deprecatedClass),
        )

    def test_readOnlyAttributes(self) -> None:
        """
        Some L{LoggingFile} attributes are read-only.
        """
        f = LoggingFile(self.logger)

        self.assertRaises(AttributeError, setattr, f, "closed", True)
        self.assertRaises(AttributeError, setattr, f, "encoding", "utf-8")
        self.assertRaises(AttributeError, setattr, f, "mode", "r")
        self.assertRaises(AttributeError, setattr, f, "newlines", ["\n"])
        self.assertRaises(AttributeError, setattr, f, "name", "foo")

    def test_unsupportedMethods(self) -> None:
        """
        Some L{LoggingFile} methods are unsupported.
        """
        f = LoggingFile(self.logger)

        self.assertRaises(IOError, f.read)
        self.assertRaises(IOError, f.next)
        self.assertRaises(IOError, f.readline)
        self.assertRaises(IOError, f.readlines)
        self.assertRaises(IOError, f.xreadlines)
        self.assertRaises(IOError, f.seek)
        self.assertRaises(IOError, f.tell)
        self.assertRaises(IOError, f.truncate)

    def test_level(self) -> None:
        """
        Default level is L{LogLevel.info} if not set.
        """
        f = LoggingFile(self.logger)
        self.assertEqual(f.level, LogLevel.info)

        f = LoggingFile(self.logger, level=LogLevel.error)
        self.assertEqual(f.level, LogLevel.error)

    def test_encoding(self) -> None:
        """
        Default encoding is C{sys.getdefaultencoding()} if not set.
        """
        f = LoggingFile(self.logger)
        self.assertEqual(f.encoding, sys.getdefaultencoding())

        f = LoggingFile(self.logger, encoding="utf-8")
        self.assertEqual(f.encoding, "utf-8")

    def test_mode(self) -> None:
        """
        Reported mode is C{"w"}.
        """
        f = LoggingFile(self.logger)
        self.assertEqual(f.mode, "w")

    def test_newlines(self) -> None:
        """
        The C{newlines} attribute is L{None}.
        """
        f = LoggingFile(self.logger)
        self.assertIsNone(f.newlines)

    def test_name(self) -> None:
        """
        The C{name} attribute is fixed.
        """
        f = LoggingFile(self.logger)
        self.assertEqual(f.name, "<LoggingFile twisted.logger.test.test_io#info>")

    def test_close(self) -> None:
        """
        L{LoggingFile.close} closes the file.
        """
        f = LoggingFile(self.logger)
        f.close()

        self.assertTrue(f.closed)
        self.assertRaises(ValueError, f.write, "Hello")

    def test_flush(self) -> None:
        """
        L{LoggingFile.flush} does nothing.
        """
        f = LoggingFile(self.logger)
        f.flush()

    def test_fileno(self) -> None:
        """
        L{LoggingFile.fileno} returns C{-1}.
        """
        f = LoggingFile(self.logger)
        self.assertEqual(f.fileno(), -1)

    def test_isatty(self) -> None:
        """
        L{LoggingFile.isatty} returns C{False}.
        """
        f = LoggingFile(self.logger)
        self.assertFalse(f.isatty())

    def test_writeBuffering(self) -> None:
        """
        Writing buffers correctly.
        """
        f = self.observedFile()
        f.write("Hello")
        self.assertEqual(f.messages, [])
        f.write(", world!\n")
        self.assertEqual(f.messages, ["Hello, world!"])
        f.write("It's nice to meet you.\n\nIndeed.")
        self.assertEqual(
            f.messages,
            [
                "Hello, world!",
                "It's nice to meet you.",
                "",
            ],
        )

    def test_writeBytesDecoded(self) -> None:
        """
        Bytes are decoded to text.
        """
        f = self.observedFile(encoding="utf-8")
        f.write(b"Hello, Mr. S\xc3\xa1nchez\n")
        self.assertEqual(f.messages, ["Hello, Mr. S\xe1nchez"])

    def test_writeUnicode(self) -> None:
        """
        Unicode is unmodified.
        """
        f = self.observedFile(encoding="utf-8")
        f.write("Hello, Mr. S\xe1nchez\n")
        self.assertEqual(f.messages, ["Hello, Mr. S\xe1nchez"])

    def test_writeLevel(self) -> None:
        """
        Log level is emitted properly.
        """
        f = self.observedFile()
        f.write("Hello\n")
        self.assertEqual(len(f.events), 1)
        self.assertEqual(f.events[0]["log_level"], LogLevel.info)

        f = self.observedFile(level=LogLevel.error)
        f.write("Hello\n")
        self.assertEqual(len(f.events), 1)
        self.assertEqual(f.events[0]["log_level"], LogLevel.error)

    def test_writeFormat(self) -> None:
        """
        Log format is C{"{message}"}.
        """
        f = self.observedFile()
        f.write("Hello\n")
        self.assertEqual(len(f.events), 1)
        self.assertEqual(f.events[0]["log_format"], "{log_io}")

    def test_writelinesBuffering(self) -> None:
        """
        C{writelines} does not add newlines.
        """
        # Note this is different behavior than t.p.log.StdioOnnaStick.
        f = self.observedFile()
        f.writelines(("Hello", ", ", ""))
        self.assertEqual(f.messages, [])
        f.writelines(("world!\n",))
        self.assertEqual(f.messages, ["Hello, world!"])
        f.writelines(("It's nice to meet you.\n\n", "Indeed."))
        self.assertEqual(
            f.messages,
            [
                "Hello, world!",
                "It's nice to meet you.",
                "",
            ],
        )

    def test_print(self) -> None:
        """
        L{LoggingFile} can replace L{sys.stdout}.
        """
        f = self.observedFile()
        self.patch(sys, "stdout", f)

        print("Hello,", end=" ")
        print("world.")

        self.assertEqual(f.messages, ["Hello, world."])

    def observedFile(
        self,
        level: NamedConstant = LogLevel.info,
        encoding: str | None = None,
    ) -> TestLoggingFile:
        """
        Construct a L{LoggingFile} with a built-in observer.

        @param level: C{level} argument to L{LoggingFile}
        @param encoding: C{encoding} argument to L{LoggingFile}

        @return: a L{TestLoggingFile} with an observer that appends received
            events into the file's C{events} attribute (a L{list}) and
            event messages into the file's C{messages} attribute (a L{list}).
        """
        # Logger takes an observer argument, for which we want to use the
        # TestLoggingFile we will create, but that takes the Logger as an
        # argument, so we'll use an array to indirectly reference the
        # TestLoggingFile.
        loggingFiles: list[TestLoggingFile] = []

        @implementer(ILogObserver)
        def observer(event: LogEvent) -> None:
            loggingFiles[0](event)

        log = Logger(observer=observer)
        loggingFiles.append(TestLoggingFile(logger=log, level=level, encoding=encoding))

        return loggingFiles[0]
