/*
 * Copyright (c) 2008-2026 Jonathan Schleifer <js@nil.im>
 *
 * All rights reserved.
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version 3.0 only,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
 * version 3.0 for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3.0 along with this program. If not, see
 * <https://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <errno.h>

#import "ObjFW.h"
#import "ObjFWTest.h"

#ifdef OF_HAIKU
# include <TypeConstants.h>
#endif

@interface OFFileManagerTests: OTTestCase
{
	OFFileManager *_fileManager;
	OFIRI *_testsDirectoryIRI, *_testFileIRI;
}
@end

@implementation OFFileManagerTests
- (void)setUp
{
	_fileManager = objc_retain([OFFileManager defaultManager]);

	_testsDirectoryIRI = objc_retain([[OFSystemInfo temporaryDirectoryIRI]
	    IRIByAppendingPathComponent: @"objfw-tests"]);
	OTAssertNotNil(_testsDirectoryIRI);

	_testFileIRI = objc_retain(
	    [_testsDirectoryIRI IRIByAppendingPathComponent: @"test.txt"]);

	/* In case a previous test run failed and left things. */
	if ([_fileManager directoryExistsAtIRI: _testsDirectoryIRI])
		[_fileManager removeItemAtIRI: _testsDirectoryIRI];

	[_fileManager createDirectoryAtIRI: _testsDirectoryIRI];
	[@"test" writeToIRI: _testFileIRI];
}

- (void)tearDown
{
	if (_testsDirectoryIRI != nil)
		[_fileManager removeItemAtIRI: _testsDirectoryIRI];
}

- (void)dealloc
{
	objc_release(_fileManager);
	objc_release(_testsDirectoryIRI);
	objc_release(_testFileIRI);

	[super dealloc];
}

- (void)testCurrentDirectoryPath
{
	OTAssertEqualObjects(
	    _fileManager.currentDirectoryPath.lastPathComponent, @"tests");
}

- (void)testAttributesOfItemAtPath
{
	OFFileAttributes attributes;

	attributes = [_fileManager attributesOfItemAtPath:
	    _testsDirectoryIRI.fileSystemRepresentation];
	OTAssertEqual(attributes.fileType, OFFileTypeDirectory);

	attributes = [_fileManager attributesOfItemAtPath:
	    _testFileIRI.fileSystemRepresentation];
	OTAssertEqual(attributes.fileType, OFFileTypeRegular);
	OTAssertEqual(attributes.fileSize, 4);
}

- (void)testSetAttributesOfItemAtPath
{
	OFDate *date = [OFDate dateWithTimeIntervalSince1970: 946681200];
	OFFileAttributes attributes;

	attributes = [OFDictionary
	    dictionaryWithObject: date
			  forKey: OFFileModificationDate];
	@try {
		[_fileManager
		    setAttributes: attributes
		     ofItemAtPath: _testFileIRI.fileSystemRepresentation];
	} @catch (OFSetItemAttributesFailedException *e) {
		if (e.errNo == ENOSYS)
			OTSkip(@"Setting modification time not supported");

		@throw e;
	}

	attributes = [_fileManager attributesOfItemAtPath:
	    _testFileIRI.fileSystemRepresentation];
	OTAssertEqual(attributes.fileType, OFFileTypeRegular);
	OTAssertEqual(attributes.fileSize, 4);
	OTAssertEqualObjects(attributes.fileModificationDate, date);
}

- (void)testFileExistsAtPath
{
	OTAssertTrue([_fileManager fileExistsAtPath:
	    _testsDirectoryIRI.fileSystemRepresentation]);
	OTAssertTrue([_fileManager fileExistsAtPath:
	    _testFileIRI.fileSystemRepresentation]);
}

- (void)testDirectoryExistsAtPath
{
	OTAssertTrue([_fileManager directoryExistsAtPath:
	    _testsDirectoryIRI.fileSystemRepresentation]);
	OTAssertFalse([_fileManager directoryExistsAtPath:
	    _testFileIRI.fileSystemRepresentation]);
}

- (void)testDirectoryAtPathCreateParents
{
	OFIRI *nestedDirectoryIRI = [_testsDirectoryIRI
	    IRIByAppendingPathComponent: @"0/1/2/3/4/5"];

	[_fileManager
	    createDirectoryAtPath: nestedDirectoryIRI.fileSystemRepresentation
		    createParents: true];
	OTAssertTrue([_fileManager directoryExistsAtPath:
	    nestedDirectoryIRI.fileSystemRepresentation]);
}

- (void)testContentsOfDirectoryAtPath
{
	OFIRI *file1IRI = [_testsDirectoryIRI
	    IRIByAppendingPathComponent: @"1.txt"];
	OFIRI *file2IRI = [_testsDirectoryIRI
	    IRIByAppendingPathComponent: @"2.txt"];

	[@"1" writeToIRI: file1IRI];
	[@"2" writeToIRI: file2IRI];

	OTAssertEqualObjects([_fileManager contentsOfDirectoryAtPath:
	    _testsDirectoryIRI.fileSystemRepresentation].sortedArray,
	    ([OFArray arrayWithObjects: @"1.txt", @"2.txt", @"test.txt", nil]));
}

- (void)testSubpathsOfDirectoryAtPath
{
	OFIRI *subdirectory1IRI = [_testsDirectoryIRI
	    IRIByAppendingPathComponent: @"a"];
	OFIRI *subdirectory2IRI = [_testsDirectoryIRI
	    IRIByAppendingPathComponent: @"b"];
	OFIRI *file1IRI = [subdirectory1IRI
	    IRIByAppendingPathComponent: @"1.txt"];
	OFIRI *file2IRI = [subdirectory2IRI
	    IRIByAppendingPathComponent: @"2.txt"];

	[_fileManager createDirectoryAtIRI: subdirectory1IRI];
	[_fileManager createDirectoryAtIRI: subdirectory2IRI];
	[@"1" writeToIRI: file1IRI];
	[@"2" writeToIRI: file2IRI];

	OTAssertEqualObjects([_fileManager subpathsOfDirectoryAtPath:
	    _testsDirectoryIRI.fileSystemRepresentation].sortedArray,
	    ([OFArray arrayWithObjects:
	    _testsDirectoryIRI.fileSystemRepresentation,
	    subdirectory1IRI.fileSystemRepresentation,
	    file1IRI.fileSystemRepresentation,
	    subdirectory2IRI.fileSystemRepresentation,
	    file2IRI.fileSystemRepresentation,
	    _testFileIRI.fileSystemRepresentation, nil]));
}

- (void)testChangeCurrentDirectoryPath
{
	OFString *oldDirectoryPath = _fileManager.currentDirectoryPath;

	OTAssertFalse([_fileManager fileExistsAtPath: @"test.txt"]);

	[_fileManager changeCurrentDirectoryPath:
	    _testsDirectoryIRI.fileSystemRepresentation];
	@try {
		/*
		 * We can't check whether currentDirectoryPath is
		 * _testsDirectoryIRI.fileSystemRepresentation because they
		 * could be different due to symlinks. Therefore check for
		 * presence of test.txt instead.
		 */
		OTAssertTrue([_fileManager fileExistsAtPath: @"test.txt"]);
	} @finally {
		[_fileManager changeCurrentDirectoryPath: oldDirectoryPath];
	}
}

- (void)testCopyItemAtPathToPath
{
	OFIRI *sourceIRI = [_testsDirectoryIRI
	    IRIByAppendingPathComponent: @"source"];
	OFIRI *destinationIRI = [_testsDirectoryIRI
	    IRIByAppendingPathComponent: @"destination"];
	OFIRI *subdirectory1IRI = [sourceIRI
	    IRIByAppendingPathComponent: @"a"];
	OFIRI *subdirectory2IRI = [sourceIRI
	    IRIByAppendingPathComponent: @"b"];
	OFIRI *file1IRI = [subdirectory1IRI
	    IRIByAppendingPathComponent: @"1.txt"];
	OFIRI *file2IRI = [subdirectory2IRI
	    IRIByAppendingPathComponent: @"2.txt"];

	[_fileManager createDirectoryAtIRI: subdirectory1IRI
			     createParents: true];
	[_fileManager createDirectoryAtIRI: subdirectory2IRI
			     createParents: true];
	[@"1" writeToIRI: file1IRI];
	[@"2" writeToIRI: file2IRI];

	subdirectory1IRI = [destinationIRI IRIByAppendingPathComponent: @"a"];
	subdirectory2IRI = [destinationIRI IRIByAppendingPathComponent: @"b"];
	file1IRI = [subdirectory1IRI IRIByAppendingPathComponent: @"1.txt"];
	file2IRI = [subdirectory2IRI IRIByAppendingPathComponent: @"2.txt"];

	OTAssertFalse([_fileManager directoryExistsAtIRI: subdirectory1IRI]);
	OTAssertFalse([_fileManager directoryExistsAtIRI: subdirectory2IRI]);
	OTAssertFalse([_fileManager fileExistsAtIRI: file1IRI]);
	OTAssertFalse([_fileManager fileExistsAtIRI: file2IRI]);

	[_fileManager copyItemAtPath: sourceIRI.fileSystemRepresentation
			      toPath: destinationIRI.fileSystemRepresentation];

	OTAssertTrue([_fileManager directoryExistsAtIRI: subdirectory1IRI]);
	OTAssertTrue([_fileManager directoryExistsAtIRI: subdirectory2IRI]);
	OTAssertEqualObjects([OFString stringWithContentsOfIRI: file1IRI],
	    @"1");
	OTAssertEqualObjects([OFString stringWithContentsOfIRI: file2IRI],
	    @"2");
}

- (void)testMoveItemAtPathToPath
{
	OFIRI *sourceIRI = [_testsDirectoryIRI
	    IRIByAppendingPathComponent: @"source"];
	OFIRI *destinationIRI = [_testsDirectoryIRI
	    IRIByAppendingPathComponent: @"destination"];
	OFIRI *subdirectory1IRI = [sourceIRI
	    IRIByAppendingPathComponent: @"a"];
	OFIRI *subdirectory2IRI = [sourceIRI
	    IRIByAppendingPathComponent: @"b"];
	OFIRI *file1IRI = [subdirectory1IRI
	    IRIByAppendingPathComponent: @"1.txt"];
	OFIRI *file2IRI = [subdirectory2IRI
	    IRIByAppendingPathComponent: @"2.txt"];

	[_fileManager createDirectoryAtIRI: subdirectory1IRI
			     createParents: true];
	[_fileManager createDirectoryAtIRI: subdirectory2IRI
			     createParents: true];
	[@"1" writeToIRI: file1IRI];
	[@"2" writeToIRI: file2IRI];

	[_fileManager moveItemAtPath: sourceIRI.fileSystemRepresentation
			      toPath: destinationIRI.fileSystemRepresentation];

	OTAssertFalse([_fileManager directoryExistsAtIRI: subdirectory1IRI]);
	OTAssertFalse([_fileManager directoryExistsAtIRI: subdirectory2IRI]);
	OTAssertFalse([_fileManager fileExistsAtIRI: file1IRI]);
	OTAssertFalse([_fileManager fileExistsAtIRI: file2IRI]);

	subdirectory1IRI = [destinationIRI IRIByAppendingPathComponent: @"a"];
	subdirectory2IRI = [destinationIRI IRIByAppendingPathComponent: @"b"];
	file1IRI = [subdirectory1IRI IRIByAppendingPathComponent: @"1.txt"];
	file2IRI = [subdirectory2IRI IRIByAppendingPathComponent: @"2.txt"];

	OTAssertTrue([_fileManager directoryExistsAtIRI: subdirectory1IRI]);
	OTAssertTrue([_fileManager directoryExistsAtIRI: subdirectory2IRI]);
	OTAssertEqualObjects([OFString stringWithContentsOfIRI: file1IRI],
	    @"1");
	OTAssertEqualObjects([OFString stringWithContentsOfIRI: file2IRI],
	    @"2");
}

- (void)testRemoveItemAtPath
{
	OFIRI *subdirectoryIRI = [_testsDirectoryIRI
	    IRIByAppendingPathComponent: @"dir"];
	OFIRI *fileIRI = [subdirectoryIRI
	    IRIByAppendingPathComponent: @"file.txt"];

	[_fileManager createDirectoryAtIRI: subdirectoryIRI];
	[@"file" writeToIRI: fileIRI];

	OTAssertTrue([_fileManager directoryExistsAtIRI: subdirectoryIRI]);
	OTAssertTrue([_fileManager fileExistsAtIRI: fileIRI]);

	[_fileManager removeItemAtPath:
	    subdirectoryIRI.fileSystemRepresentation];

	OTAssertFalse([_fileManager fileExistsAtIRI: fileIRI]);
	OTAssertFalse([_fileManager directoryExistsAtIRI: subdirectoryIRI]);
}

#ifdef OF_FILE_MANAGER_SUPPORTS_LINKS
- (void)testLinkItemAtPathToPath
{
	OFIRI *sourceIRI = [_testsDirectoryIRI
	    IRIByAppendingPathComponent: @"source"];
	OFIRI *destinationIRI = [_testsDirectoryIRI
	    IRIByAppendingPathComponent: @"destination"];
	OFFileAttributes attributes;

	[@"test" writeToIRI: sourceIRI];

	@try {
		[_fileManager
		    linkItemAtPath: sourceIRI.fileSystemRepresentation
			    toPath: destinationIRI.fileSystemRepresentation];
	} @catch (OFLinkItemFailedException *e) {
		if (e.errNo == EPERM)
			OTSkip(@"Links not supported");

		@throw e;
	} @catch (OFNotImplementedException *e) {
		OTSkip(@"Links not supported");
	}

	attributes = [_fileManager attributesOfItemAtIRI: destinationIRI];
	OTAssertEqual(attributes.fileType, OFFileTypeRegular);
	OTAssertEqual(attributes.fileSize, 4);
	OTAssertEqualObjects([OFString stringWithContentsOfIRI: destinationIRI],
	    @"test");
}
#endif

#ifdef OF_FILE_MANAGER_SUPPORTS_SYMLINKS
- (void)testCreateSymbolicLinkAtPathWithDestinationPath
{
	OFIRI *sourceIRI, *destinationIRI;
	OFFileAttributes attributes;

# ifdef OF_WINDOWS
	if ([OFSystemInfo wineVersion] != nil)
		OTSkip(@"Wine creates broken symlinks");
# endif

	sourceIRI = [_testsDirectoryIRI IRIByAppendingPathComponent: @"source"];
	destinationIRI = [_testsDirectoryIRI
	    IRIByAppendingPathComponent: @"destination"];

	[@"test" writeToIRI: sourceIRI];

	@try {
		OFString *sourcePath = sourceIRI.fileSystemRepresentation;
		OFString *destinationPath =
		    destinationIRI.fileSystemRepresentation;

		[_fileManager createSymbolicLinkAtPath: destinationPath
				   withDestinationPath: sourcePath];
	} @catch (OFCreateSymbolicLinkFailedException *e) {
		if (e.errNo != EPERM)
			@throw e;

		OTSkip(@"No permission to create symlink.\n"
		    @"On Windows, only the administrator can create symbolic "
		    @"links.");
	} @catch (OFNotImplementedException *e) {
		OTSkip(@"Symlinks not supported");
	}

	attributes = [_fileManager attributesOfItemAtIRI: destinationIRI];
	OTAssertEqual(attributes.fileType, OFFileTypeSymbolicLink);
	OTAssertEqualObjects([OFString stringWithContentsOfIRI: destinationIRI],
	    @"test");
}
#endif

#ifdef OF_FILE_MANAGER_SUPPORTS_EXTENDED_ATTRIBUTES
- (void)testExtendedAttributes
{
	OFData *data = [OFData dataWithItems: "test" count: 4];
	OFString *testFilePath = _testFileIRI.fileSystemRepresentation;
	OFFileAttributes attributes;
	OFArray *extendedAttributeNames;

	@try {
		[_fileManager setExtendedAttributeData: data
					       forName: @"user.test"
					  ofItemAtPath: testFilePath];
	} @catch (OFSetItemAttributesFailedException *e) {
		if (e.errNo != ENOTSUP && e.errNo != EOPNOTSUPP)
			@throw e;

		OTSkip(@"Extended attributes are not supported");
	}

	attributes = [_fileManager attributesOfItemAtIRI: _testFileIRI];
	extendedAttributeNames =
	    [attributes objectForKey: OFFileExtendedAttributesNames];
	OTAssertNotNil(extendedAttributeNames);
	OTAssertTrue([extendedAttributeNames containsObject: @"user.test"]);
	OTAssertEqualObjects(
	    [_fileManager extendedAttributeDataForName: @"user.test"
					  ofItemAtPath: testFilePath],
	    data);

	[_fileManager removeExtendedAttributeForName: @"user.test"
					ofItemAtPath: testFilePath];

	attributes = [_fileManager attributesOfItemAtIRI: _testFileIRI];
	extendedAttributeNames =
	    [attributes objectForKey: OFFileExtendedAttributesNames];
	OTAssertNotNil(extendedAttributeNames);
	OTAssertFalse([extendedAttributeNames containsObject: @"user.test"]);
	OTAssertThrowsSpecific(
	    [_fileManager extendedAttributeDataForName: @"user.test"
					  ofItemAtPath: testFilePath],
	    OFGetItemAttributesFailedException);
}
#endif

#ifdef OF_HAIKU
- (void)testGetExtendedAttributeDataAndTypeForNameOfItemAtPath
{
	OFData *data;
	id type;

	[_fileManager getExtendedAttributeData: &data
				       andType: &type
				       forName: @"BEOS:TYPE"
				  ofItemAtPath: @"/boot/system/lib/libbe.so"];
	OTAssertEqualObjects(type,
	    [OFNumber numberWithUnsignedLong: B_MIME_STRING_TYPE]);
	OTAssertEqualObjects(data,
	    [OFData dataWithItems: "application/x-vnd.Be-elfexecutable"
			    count: 35]);
}

- (void)testSetExtendedAttributeDataAndTypeForNameOfItemAtPath
{
	OFString *testFilePath = _testFileIRI.fileSystemRepresentation;
	OFData *data, *expectedData = [OFData dataWithItems: "foobar" count: 6];
	id type, expectedType = [OFNumber numberWithUnsignedLong: 1234];

	[_fileManager setExtendedAttributeData: expectedData
				       andType: expectedType
				       forName: @"testattribute"
				  ofItemAtPath: testFilePath];

	[_fileManager getExtendedAttributeData: &data
				       andType: &type
				       forName: @"testattribute"
				  ofItemAtPath: testFilePath];

	OTAssertEqualObjects(data, expectedData);
	OTAssertEqualObjects(type, expectedType);

	[_fileManager removeExtendedAttributeForName: @"testattribute"
					ofItemAtPath: testFilePath];

	OTAssertThrowsSpecific(
	    [_fileManager getExtendedAttributeData: &data
					   andType: &type
					   forName: @"testattribute"
				      ofItemAtPath: testFilePath],
	    OFGetItemAttributesFailedException);
}
#endif
@end
