/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to you under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.logging.log4j.core.appender.rolling;

import static org.apache.logging.log4j.core.test.hamcrest.FileMatchers.beforeNow;
import static org.apache.logging.log4j.core.test.hamcrest.FileMatchers.hasLength;
import static org.apache.logging.log4j.core.test.hamcrest.FileMatchers.isEmpty;
import static org.apache.logging.log4j.core.test.hamcrest.FileMatchers.lastModified;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Set;
import java.util.concurrent.locks.LockSupport;
import org.apache.logging.log4j.core.config.DefaultConfiguration;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.apache.logging.log4j.core.util.Closer;
import org.apache.logging.log4j.core.util.FileUtils;
import org.apache.logging.log4j.core.util.NullOutputStream;
import org.apache.logging.log4j.util.Strings;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

/**
 * Tests the RollingRandomAccessFileManager class.
 */
class RollingRandomAccessFileManagerTest {

    /**
     * Test method for
     * {@link org.apache.logging.log4j.core.appender.rolling.RollingRandomAccessFileManager#writeBytes(byte[], int, int)}
     */
    @Test
    void testWrite_multiplesOfBufferSize() throws IOException {
        final File file = File.createTempFile("log4j2", "test");
        file.deleteOnExit();
        try (final RandomAccessFile raf = new RandomAccessFile(file, "rw")) {
            final OutputStream os = NullOutputStream.getInstance();
            final boolean append = false;
            final boolean flushNow = false;
            final long triggerSize = Long.MAX_VALUE;
            final long time = System.currentTimeMillis();
            final TriggeringPolicy triggerPolicy = new SizeBasedTriggeringPolicy(triggerSize);
            final RolloverStrategy rolloverStrategy = null;
            final RollingRandomAccessFileManager manager = new RollingRandomAccessFileManager(
                    null,
                    raf,
                    file.getName(),
                    Strings.EMPTY,
                    os,
                    append,
                    flushNow,
                    RollingRandomAccessFileManager.DEFAULT_BUFFER_SIZE,
                    triggerSize,
                    time,
                    triggerPolicy,
                    rolloverStrategy,
                    null,
                    null,
                    null,
                    null,
                    null,
                    true);

            final int size = RollingRandomAccessFileManager.DEFAULT_BUFFER_SIZE * 3;
            final byte[] data = new byte[size];
            manager.write(data, 0, data.length, flushNow); // no buffer overflow exception

            // buffer is full but not flushed yet
            assertEquals(RollingRandomAccessFileManager.DEFAULT_BUFFER_SIZE * 3, raf.length());
        }
    }

    /**
     * Test method for
     * {@link org.apache.logging.log4j.core.appender.rolling.RollingRandomAccessFileManager#writeBytes(byte[], int, int)} .
     */
    @Test
    void testWrite_dataExceedingBufferSize() throws IOException {
        final File file = File.createTempFile("log4j2", "test");
        file.deleteOnExit();
        try (final RandomAccessFile raf = new RandomAccessFile(file, "rw")) {
            final OutputStream os = NullOutputStream.getInstance();
            final boolean append = false;
            final boolean flushNow = false;
            final long triggerSize = 0;
            final long time = System.currentTimeMillis();
            final TriggeringPolicy triggerPolicy = new SizeBasedTriggeringPolicy(triggerSize);
            final RolloverStrategy rolloverStrategy = null;
            final RollingRandomAccessFileManager manager = new RollingRandomAccessFileManager(
                    null,
                    raf,
                    file.getName(),
                    Strings.EMPTY,
                    os,
                    append,
                    flushNow,
                    RollingRandomAccessFileManager.DEFAULT_BUFFER_SIZE,
                    triggerSize,
                    time,
                    triggerPolicy,
                    rolloverStrategy,
                    null,
                    null,
                    null,
                    null,
                    null,
                    true);

            final int size = RollingRandomAccessFileManager.DEFAULT_BUFFER_SIZE * 3 + 1;
            final byte[] data = new byte[size];
            manager.write(data, 0, data.length, flushNow); // no exception
            assertEquals(RollingRandomAccessFileManager.DEFAULT_BUFFER_SIZE * 3 + 1, raf.length());

            manager.flush();
            assertEquals(size, raf.length()); // all data written to file now
        }
    }

    @Test
    void testConfigurableBufferSize() throws IOException {
        final File file = File.createTempFile("log4j2", "test");
        file.deleteOnExit();
        try (final RandomAccessFile raf = new RandomAccessFile(file, "rw")) {
            final OutputStream os = NullOutputStream.getInstance();
            final boolean append = false;
            final boolean flushNow = false;
            final long triggerSize = 0;
            final long time = System.currentTimeMillis();
            final TriggeringPolicy triggerPolicy = new SizeBasedTriggeringPolicy(triggerSize);
            final int bufferSize = 4 * 1024;
            assertNotEquals(bufferSize, RollingRandomAccessFileManager.DEFAULT_BUFFER_SIZE);
            final RolloverStrategy rolloverStrategy = null;
            final RollingRandomAccessFileManager manager = new RollingRandomAccessFileManager(
                    null,
                    raf,
                    file.getName(),
                    Strings.EMPTY,
                    os,
                    append,
                    flushNow,
                    bufferSize,
                    triggerSize,
                    time,
                    triggerPolicy,
                    rolloverStrategy,
                    null,
                    null,
                    null,
                    null,
                    null,
                    true);

            // check the resulting buffer size is what was requested
            assertEquals(bufferSize, manager.getBufferSize());
        }
    }

    @Test
    void testAppendDoesNotOverwriteExistingFile() throws IOException {
        final boolean isAppend = true;
        final File file = File.createTempFile("log4j2", "test");
        file.deleteOnExit();
        assertThat(file, isEmpty());

        final byte[] bytes = new byte[4 * 1024];

        // create existing file
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(file);
            fos.write(bytes, 0, bytes.length);
            fos.flush();
        } finally {
            Closer.closeSilently(fos);
        }
        assertThat("all flushed to disk", file, hasLength(bytes.length));

        final boolean immediateFlush = true;
        final RollingRandomAccessFileManager manager = RollingRandomAccessFileManager.getRollingRandomAccessFileManager(
                //
                file.getAbsolutePath(),
                Strings.EMPTY,
                isAppend,
                immediateFlush,
                RollingRandomAccessFileManager.DEFAULT_BUFFER_SIZE,
                new SizeBasedTriggeringPolicy(Long.MAX_VALUE), //
                null,
                null,
                null,
                null,
                null,
                null,
                null);
        manager.write(bytes, 0, bytes.length, immediateFlush);
        final int expected = bytes.length * 2;
        assertThat("appended, not overwritten", file, hasLength(expected));
    }

    @Test
    void testFileTimeBasedOnSystemClockWhenAppendIsFalse() throws IOException {
        final File file = File.createTempFile("log4j2", "test");
        file.deleteOnExit();
        LockSupport.parkNanos(1000000); // 1 millisec

        // append is false deletes the file if it exists
        final boolean isAppend = false;
        final long expectedMin = System.currentTimeMillis();
        final long expectedMax = expectedMin + 500;
        assertThat(file, lastModified(lessThanOrEqualTo(expectedMin)));

        final RollingRandomAccessFileManager manager = RollingRandomAccessFileManager.getRollingRandomAccessFileManager(
                //
                file.getAbsolutePath(),
                Strings.EMPTY,
                isAppend,
                true,
                RollingRandomAccessFileManager.DEFAULT_BUFFER_SIZE,
                new SizeBasedTriggeringPolicy(Long.MAX_VALUE), //
                null,
                null,
                null,
                null,
                null,
                null,
                null);
        assertTrue(manager.getFileTime() < expectedMax);
        assertTrue(manager.getFileTime() >= expectedMin);
    }

    @Test
    void testFileTimeBasedOnFileModifiedTimeWhenAppendIsTrue() throws IOException {
        final File file = File.createTempFile("log4j2", "test");
        file.deleteOnExit();
        LockSupport.parkNanos(1000000); // 1 millisec

        final boolean isAppend = true;
        assertThat(file, lastModified(beforeNow()));

        final RollingRandomAccessFileManager manager = RollingRandomAccessFileManager.getRollingRandomAccessFileManager(
                //
                file.getAbsolutePath(),
                Strings.EMPTY,
                isAppend,
                true,
                RollingRandomAccessFileManager.DEFAULT_BUFFER_SIZE,
                new SizeBasedTriggeringPolicy(Long.MAX_VALUE), //
                null,
                null,
                null,
                null,
                null,
                null,
                null);
        assertThat(file, lastModified(equalTo(manager.getFileTime())));
    }

    @Test
    void testRolloverRetainsFileAttributes() throws Exception {

        // Short-circuit if host doesn't support file attributes.
        if (!FileUtils.isFilePosixAttributeViewSupported()) {
            return;
        }

        // Create the initial file.
        final File file = File.createTempFile("log4j2", "test");
        LockSupport.parkNanos(1000000); // 1 millisec

        // Set the initial file attributes.
        final String filePermissionsString = "rwxrwxrwx";
        final Set<PosixFilePermission> filePermissions = PosixFilePermissions.fromString(filePermissionsString);
        FileUtils.defineFilePosixAttributeView(file.toPath(), filePermissions, null, null);

        // Create the manager.
        final RolloverStrategy rolloverStrategy = DefaultRolloverStrategy.newBuilder()
                .setMax("7")
                .setMin("1")
                .setFileIndex("max")
                .setStopCustomActionsOnError(false)
                .setConfig(new DefaultConfiguration())
                .build();
        final RollingRandomAccessFileManager manager = RollingRandomAccessFileManager.getRollingRandomAccessFileManager(
                file.getAbsolutePath(),
                Strings.EMPTY,
                true,
                true,
                RollingRandomAccessFileManager.DEFAULT_BUFFER_SIZE,
                new SizeBasedTriggeringPolicy(Long.MAX_VALUE),
                rolloverStrategy,
                null,
                null,
                filePermissionsString,
                null,
                null,
                null);
        assertNotNull(manager);
        manager.initialize();

        // Trigger a rollover.
        manager.rollover();

        // Verify the rolled over file attributes.
        final Set<PosixFilePermission> actualFilePermissions = Files.getFileAttributeView(
                        Paths.get(manager.getFileName()), PosixFileAttributeView.class)
                .readAttributes()
                .permissions();
        assertEquals(filePermissions, actualFilePermissions);
    }

    @ParameterizedTest
    @CsvSource({
        "true,true",
        "true,false",
        "false,true",
        "false,false",
    })
    void testWriteHeaderWhetherAppendOrExists(final boolean append, final boolean fileExists) throws Exception {
        final File file = File.createTempFile("log4j2", "test");
        if (!fileExists) {
            file.delete(); // Ensure file doesn't exist
        } else {
            // If file exists, write some content to it so it's not empty
            try (FileOutputStream fos = new FileOutputStream(file)) {
                fos.write("EXISTING_CONTENT".getBytes());
            }
        }
        file.deleteOnExit();

        final String header = "HEADER";
        final PatternLayout layout =
                PatternLayout.newBuilder().setHeader(header).build();

        final RollingRandomAccessFileManager manager = RollingRandomAccessFileManager.getRollingRandomAccessFileManager(
                file.getAbsolutePath(),
                Strings.EMPTY,
                append,
                true,
                RollingRandomAccessFileManager.DEFAULT_BUFFER_SIZE,
                new SizeBasedTriggeringPolicy(Long.MAX_VALUE),
                null,
                null,
                layout,
                null,
                null,
                null,
                null);
        assertNotNull(manager);
        manager.close();

        // Verify header was written based on logic: writeHeader = !append || !fileExistedBefore
        // Note: writeHeader() also checks file.length() == 0, so header is only written if file is empty
        assertTrue(file.exists(), "File should exist");
        final byte[] fileContent = Files.readAllBytes(file.toPath());
        final String content = new String(fileContent);
        final boolean expectedHeaderWritten = !append || !fileExists;
        if (expectedHeaderWritten) {
            assertTrue(
                    content.startsWith(header),
                    "File should start with header when append=" + append + ", fileExists=" + fileExists + ", content: "
                            + content);
        } else {
            // When append=true and fileExists=true, file has existing content, so header should not be written
            assertTrue(
                    !content.startsWith(header),
                    "File should not start with header when append=" + append + ", fileExists=" + fileExists
                            + ", content: " + content);
        }
    }
}
