import XCTest
import AppKit
import CryptoKit
@testable import PoseCore

final class SessionDiscoveryTests: XCTestCase {
    func testCreatesUnannotatedManifestAndReusesResults() throws {
        let root = try temporaryDirectory(); defer { try? FileManager.default.removeItem(at: root) }
        let folder = root.appendingPathComponent("Older pictures")
        try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
        try image().write(to: folder.appendingPathComponent("ride.PNG"))
        try FileManager.default.createDirectory(at: root.appendingPathComponent("empty"), withIntermediateDirectories: true)
        var calls = 0
        let first = SessionDiscovery.run(dataDirectory: root, analyze: { manifest, output in
            calls += 1; try self.writeReport(manifest: manifest, output: output)
        })
        XCTAssertTrue(first.failures.isEmpty)
        XCTAssertEqual(first.sessions.count, 1)
        XCTAssertTrue(try XCTUnwrap(first.sessions.first).wasAnalyzed)
        let manifestURL = folder.appendingPathComponent("session.json")
        let original = try Data(contentsOf: manifestURL)
        let manifest = try SessionManifest.load(url: manifestURL)
        XCTAssertEqual(manifest.pairs[0].riders[0].panel, PixelRect(x: 0, y: 0, width: 12, height: 8))
        XCTAssertTrue(manifest.pairs[0].riders[0].annotations.isEmpty)
        let second = SessionDiscovery.run(dataDirectory: root, analyze: { _, _ in calls += 1 })
        XCTAssertEqual(calls, 1)
        XCTAssertEqual(second.sessions.first?.resultsURL, first.sessions.first?.resultsURL)
        XCTAssertEqual(try Data(contentsOf: manifestURL), original)
    }

    func testFailureDoesNotStopOtherFoldersAndFailedOutputIsPreserved() throws {
        let root = try temporaryDirectory(); defer { try? FileManager.default.removeItem(at: root) }
        for name in ["bad", "good"] {
            let folder = root.appendingPathComponent(name)
            try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
            try image().write(to: folder.appendingPathComponent("ride.png"))
        }
        var failedOutput: URL?
        let result = SessionDiscovery.run(dataDirectory: root, analyze: { manifest, output in
            if manifest.deletingLastPathComponent().lastPathComponent == "bad" {
                try FileManager.default.createDirectory(at: output, withIntermediateDirectories: true)
                failedOutput = output
                throw PoseError.invalidInput("Synthetic failure")
            }
            try self.writeReport(manifest: manifest, output: output)
        })
        XCTAssertEqual(result.sessions.count, 1)
        XCTAssertEqual(result.failures.count, 1)
        XCTAssertTrue(FileManager.default.fileExists(atPath: try XCTUnwrap(failedOutput).path))
        let retry = SessionDiscovery.run(dataDirectory: root, analyze: { manifest, output in
            XCTAssertNotEqual(output, failedOutput)
            try self.writeReport(manifest: manifest, output: output)
        })
        XCTAssertEqual(retry.sessions.count, 2)
        XCTAssertTrue(retry.failures.isEmpty)
    }

    func testLegacyResultReuseAndUnlistedPhotosPreserveManifest() throws {
        let root = try temporaryDirectory(); defer { try? FileManager.default.removeItem(at: root) }
        let folder = root.appendingPathComponent("session")
        try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
        try image().write(to: folder.appendingPathComponent("ride.png"))
        let initial = SessionDiscovery.run(dataDirectory: root, analyze: writeReport)
        let session = try XCTUnwrap(initial.sessions.first)
        let original = try Data(contentsOf: session.manifestURL)
        let legacy = root.appendingPathComponent("legacy-output")
        try FileManager.default.moveItem(at: session.resultsURL.deletingLastPathComponent(), to: legacy)
        try image().write(to: folder.appendingPathComponent("new.png"))
        let result = SessionDiscovery.run(dataDirectory: root,
            knownResults: [session.manifestURL: legacy.appendingPathComponent("results.json")],
            analyze: { _, _ in XCTFail("Valid legacy output should be reused") })
        XCTAssertEqual(result.sessions.count, 1)
        XCTAssertEqual(result.failures.count, 1)
        XCTAssertTrue(result.failures[0].message.contains("not listed"))
        XCTAssertEqual(try Data(contentsOf: session.manifestURL), original)
    }

    func testSourceChangesInvalidateResultsWithoutOverwritingThem() throws {
        let root = try temporaryDirectory(); defer { try? FileManager.default.removeItem(at: root) }
        let folder = root.appendingPathComponent("session")
        try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
        let photo = folder.appendingPathComponent("ride.png")
        try image().write(to: photo)
        let first = SessionDiscovery.run(dataDirectory: root, analyze: writeReport)
        let firstURL = try XCTUnwrap(first.sessions.first?.resultsURL)
        let oldResult = try Data(contentsOf: firstURL)
        var changed = try Data(contentsOf: photo); changed.append(0)
        try changed.write(to: photo)
        var calls = 0
        let second = SessionDiscovery.run(dataDirectory: root, analyze: { manifest, output in
            calls += 1; try self.writeReport(manifest: manifest, output: output)
        })
        XCTAssertEqual(calls, 1)
        XCTAssertNotEqual(second.sessions.first?.resultsURL, firstURL)
        XCTAssertEqual(try Data(contentsOf: firstURL), oldResult)
    }

    func testSkipsHiddenAndSymlinkFolders() throws {
        let root = try temporaryDirectory(); defer { try? FileManager.default.removeItem(at: root) }
        let outside = try temporaryDirectory(); defer { try? FileManager.default.removeItem(at: outside) }
        try image().write(to: outside.appendingPathComponent("ride.png"))
        try FileManager.default.createSymbolicLink(at: root.appendingPathComponent("linked"), withDestinationURL: outside)
        let hidden = root.appendingPathComponent(".hidden")
        try FileManager.default.createDirectory(at: hidden, withIntermediateDirectories: true)
        try image().write(to: hidden.appendingPathComponent("ride.png"))
        let result = SessionDiscovery.run(dataDirectory: root, analyze: { _, _ in XCTFail("No eligible sessions") })
        XCTAssertTrue(result.sessions.isEmpty)
        XCTAssertTrue(result.failures.isEmpty)
        XCTAssertFalse(FileManager.default.fileExists(atPath: outside.appendingPathComponent("session.json").path))
    }

    func testSavedReviewBindingWinsOverEarlierUnsavedReport() throws {
        let root = try temporaryDirectory(); defer { try? FileManager.default.removeItem(at: root) }
        let folder = root.appendingPathComponent("session")
        try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
        try image().write(to: folder.appendingPathComponent("ride.png"))
        let initial = SessionDiscovery.run(dataDirectory: root, analyze: writeReport)
        let session = try XCTUnwrap(initial.sessions.first)
        let unsavedURL = folder.appendingPathComponent("results.json")
        var report = try JSONDecoder().decode(FeasibilityReport.self, from: Data(contentsOf: session.resultsURL))
        report.generatedAt = "a different run"
        try JSONEncoder().encode(report).write(to: unsavedURL)
        var saved = try ReviewStore.make(manifestURL: session.manifestURL, reportURL: session.resultsURL)
        saved.revision = 7
        let savedURL = folder.appendingPathComponent("joint-review-\(saved.reportHash.prefix(12)).json")
        try ReviewStore.save(saved, to: savedURL, expectedRevision: nil)
        let result = SessionDiscovery.run(dataDirectory: root,
            knownResults: [session.manifestURL: unsavedURL],
            analyze: { _, _ in XCTFail("Both reports already match") })
        XCTAssertEqual(result.sessions.first?.resultsURL, session.resultsURL)
        XCTAssertEqual(try ReviewStore.load(from: savedURL).revision, 7)

        var otherSaved = try ReviewStore.make(manifestURL: session.manifestURL, reportURL: unsavedURL)
        otherSaved.revision = 1
        let otherURL = folder.appendingPathComponent("joint-review-\(otherSaved.reportHash.prefix(12)).json")
        try ReviewStore.save(otherSaved, to: otherURL, expectedRevision: nil)
        try FileManager.default.setAttributes([.modificationDate: Date(timeIntervalSince1970: 100)], ofItemAtPath: savedURL.path)
        try FileManager.default.setAttributes([.modificationDate: Date(timeIntervalSince1970: 200)], ofItemAtPath: otherURL.path)
        let newest = SessionDiscovery.run(dataDirectory: root, analyze: { _, _ in XCTFail("Cached") })
        XCTAssertEqual(newest.sessions.first?.resultsURL.resolvingSymlinksInPath(), unsavedURL.resolvingSymlinksInPath())
        let configured = SessionDiscovery.run(dataDirectory: root,
            knownResults: [session.manifestURL: session.resultsURL], analyze: { _, _ in XCTFail("Cached") })
        XCTAssertEqual(configured.sessions.first?.resultsURL.resolvingSymlinksInPath(), session.resultsURL.resolvingSymlinksInPath())
    }

    func testMissingDataFolderReportsFailure() throws {
        let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
        let result = SessionDiscovery.run(dataDirectory: root, analyze: { _, _ in XCTFail("Missing root") })
        XCTAssertTrue(result.sessions.isEmpty)
        XCTAssertEqual(result.failures.count, 1)
        XCTAssertEqual(result.failures.first?.folderURL, root.resolvingSymlinksInPath())
    }

    private func temporaryDirectory() throws -> URL {
        let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
        try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
        return url
    }
    private func image() -> Data {
        let bitmap = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: 12, pixelsHigh: 8,
            bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false,
            colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0)!
        return bitmap.representation(using: .png, properties: [:])!
    }
    private func writeReport(manifest url: URL, output: URL) throws {
        let manifest = try SessionManifest.load(url: url)
        let hash = SHA256.hash(data: try Data(contentsOf: url)).map { String(format: "%02x", $0) }.joined()
        let report = FeasibilityReport(session: manifest.session, operatingSystem: "test", machineArchitecture: "test",
            generatedAt: "test", annotationProvenance: manifest.annotationProvenance, manifestHash: hash,
            crops: try manifest.pairs.flatMap { pair in try pair.riders.map { rider in
                let asset = try manifest.assetURL(for: pair, rider: rider, manifestURL: url)
                let sourceHash = SHA256.hash(data: try Data(contentsOf: asset)).map { String(format: "%02x", $0) }.joined()
                return CropReport(pairID: pair.id, role: rider.role, rider: rider.rider, sourceHash: sourceHash, crop: rider.crop)
            } })
        try FileManager.default.createDirectory(at: output, withIntermediateDirectories: true)
        try JSONEncoder().encode(report).write(to: output.appendingPathComponent("results.json"))
    }
}
