import XCTest
@testable import PoseCore

final class LeanReviewTests: XCTestCase {
    func testSchemaTwoBackupFailuresAndForeignEvidencePreserveReview() throws {
        let folder = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
        try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
        defer { try? FileManager.default.removeItem(at: folder) }
        let url = folder.appendingPathComponent("review.json")
        var old = pairDocument(); old.schemaVersion = 2
        let bytes = try JSONEncoder().encode(old); try bytes.write(to: url)
        let backup = ReviewStore.legacyBackupURL(for: url, schema: 2)
        var next = old; next.schemaVersion = 3; next.revision = 1
        next.leanAnnotations[next.frames[0].id] = annotation(next.frames[0])
        try Data("mismatch".utf8).write(to: backup)
        XCTAssertThrowsError(try ReviewStore.save(next, to: url, expectedRevision: 0))
        XCTAssertEqual(try Data(contentsOf: url), bytes)
        try FileManager.default.removeItem(at: backup)
        try FileManager.default.createSymbolicLink(at: backup, withDestinationURL: url)
        XCTAssertThrowsError(try ReviewStore.save(next, to: url, expectedRevision: 0))
        XCTAssertEqual(try Data(contentsOf: url), bytes)
        try FileManager.default.removeItem(at: backup)
        next.leanAnnotations[next.frames[0].id]?.wheel?.uncertainty = -1
        XCTAssertThrowsError(try ReviewStore.save(next, to: url, expectedRevision: 0))
        XCTAssertFalse(FileManager.default.fileExists(atPath: backup.path))
        next.leanAnnotations[next.frames[0].id] = annotation(next.frames[1])
        XCTAssertThrowsError(try ReviewStore.save(next, to: url, expectedRevision: 0))
        XCTAssertEqual(try Data(contentsOf: url), bytes)
    }
    func testSharedFeatureDifferenceAndSpeedNeverBypassApproval() throws {
        var doc = pairDocument()
        let pair = try XCTUnwrap(PairReview.resolve(doc.frames).first)
        func axis(_ theta: Double, roll: Double) -> LeanLine {
            let radians = (theta + roll) * Double.pi / 180
            return LeanLine(a: Point2(x: 250, y: 250), b: Point2(x: 250+150*sin(radians), y: 250-150*cos(radians)), uncertainty: 1)
        }
        for f in doc.frames {
            let isSubject = f.role == "subject", roll = f.role == "subject" ? 12.0 : -17.0
            var a = annotation(f); a.wheel = axis(isSubject ? 35 : 25, roll: roll)
            a.reference = axis(80, roll: roll); a.basis = .sharedFeature
            a.referenceReviewed = true; a.featureID = "same-two-stationary-points"
            doc.leanAnnotations[f.id] = a
        }
        var context = LeanPairContext(); context.similarSpeedReported = true
        doc.leanContexts[pair.id] = context
        XCTAssertNil(try LeanAnalysis.compare(doc, pair: pair).measurement)
        context.commonCue = true; context.comparableHeading = true; context.comparableView = true; context.comparableReference = true
        context.approvalDigest = try LeanAnalysis.evidenceDigest(doc, pair: pair); doc.leanContexts[pair.id] = context
        let comparison = try LeanAnalysis.compare(doc, pair: pair)
        XCTAssertEqual(try XCTUnwrap(comparison.measurement).angle, 10, accuracy: 1e-8)
        XCTAssertTrue(comparison.summary.contains("image right"))
        doc.leanAnnotations[pair.subjectID]?.featureID = "different-road-edge-tangent"
        XCTAssertNil(try LeanAnalysis.compare(doc, pair: pair).measurement)
        doc.leanAnnotations[pair.subjectID]?.basis = .imageVertical
        XCTAssertNil(try LeanAnalysis.compare(doc, pair: pair).measurement)
    }
    func annotation(_ f: ReviewFrame) -> LeanAnnotation {
        var a = LeanAnnotation(frame: f, wheel: LeanLine(a: Point2(x: 100, y: 300), b: Point2(x: 160, y: 100), uncertainty: 3))
        a.state = .reviewed
        return a
    }
    func testLegacyBackupsAndImporterPreserveLean() throws {
        for version in [1, 2] {
            let folder = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
            try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
            defer { try? FileManager.default.removeItem(at: folder) }
            let url = folder.appendingPathComponent("review.json")
            var doc = pairDocument(); doc.schemaVersion = version
            let bytes = try JSONEncoder().encode(doc); try bytes.write(to: url)
            var loaded = try ReviewStore.load(from: url)
            XCTAssertTrue(loaded.leanAnnotations.isEmpty)
            XCTAssertEqual(try Data(contentsOf: url), bytes)
            loaded.leanAnnotations[loaded.frames[0].id] = annotation(loaded.frames[0])
            loaded.schemaVersion = 3; loaded.revision += 1
            try ReviewStore.save(loaded, to: url, expectedRevision: 0)
            XCTAssertEqual(try Data(contentsOf: ReviewStore.legacyBackupURL(for: url, schema: version)), bytes)
            let reopened = try ReviewStore.load(from: url)
            XCTAssertEqual(reopened.schemaVersion, 3)
            XCTAssertEqual(reopened.leanAnnotations, loaded.leanAnnotations)
            let pair = try XCTUnwrap(PairReview.resolve(loaded.frames).first)
            var context = try PairContext(document: loaded, pair: pair)
            context.photoCoaching = PhotoCoachingReview(cornerDirection: .left, provenance: "test", points: [PhotoCoachingPoint(title: "Note", detail: "Authored context")])
            let imported = try PhotoCoachingImport.apply([context], to: reopened)
            XCTAssertEqual(imported.schemaVersion, 3)
            XCTAssertEqual(imported.leanAnnotations, reopened.leanAnnotations)
        }
    }
    func testApprovalBoundToEvidenceAndMalformedDataRejected() throws {
        var doc = pairDocument()
        for f in doc.frames { doc.leanAnnotations[f.id] = annotation(f) }
        let pair = try XCTUnwrap(PairReview.resolve(doc.frames).first)
        XCTAssertNil(try LeanAnalysis.compare(doc, pair: pair).measurement)
        var context = LeanPairContext()
        context.commonCue = true; context.comparableView = true; context.comparableHeading = true
        context.comparableReference = true; context.comparableCameraRoll = true
        context.approvalDigest = try LeanAnalysis.evidenceDigest(doc, pair: pair)
        doc.leanContexts[pair.id] = context
        let report = try LeanAnalysis.compare(doc, pair: pair)
        XCTAssertNotNil(report.measurement)
        XCTAssertTrue(report.summary.contains("unresolved"))
        doc.leanAnnotations[pair.subjectID]?.wheel?.a.x += 10
        XCTAssertNil(try LeanAnalysis.compare(doc, pair: pair).measurement)
        doc.leanAnnotations["orphan"] = annotation(doc.frames[0])
        XCTAssertThrowsError(try LeanReview.validate(doc))
    }
}
