import XCTest
@testable import PoseCore

func pairFrame(_ role: String, pair: String = "P01") -> ReviewFrame {
    ReviewFrame(id: "\(pair)-\(role)", pairID: pair, role: role, rider: role,
                file: "\(role).png", sourceHash: role, panel: PixelRect(x: 0, y: 0, width: 500, height: 500), raw3D: nil, raw2D: nil)
}
func pairDocument() -> ReviewDocument {
    ReviewDocument(manifestHash: "manifest", reportHash: "report", frames: [pairFrame("reference"), pairFrame("subject")])
}
final class PairReviewTests: XCTestCase {
    func testRolesAndMissingReference() throws {
        let pairs = try PairReview.resolve(pairDocument().frames)
        XCTAssertEqual(pairs.first?.subjectID, "P01-subject")
        XCTAssertEqual(pairs.first?.referenceID, "P01-reference")
        XCTAssertNil(try PairReview.resolve([pairFrame("subject")]).first?.referenceID)
        XCTAssertThrowsError(try PairReview.resolve([pairFrame("reference")]))
        XCTAssertThrowsError(try PairReview.resolve([pairFrame("subject"), pairFrame("subject")]))
    }
    func testContextBindingAndLegacyBackup() 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 legacy = pairDocument(); legacy.schemaVersion = 1
        let bytes = try JSONEncoder().encode(legacy)
        try bytes.write(to: url)
        var loaded = try ReviewStore.load(from: url)
        let pair = try XCTUnwrap(PairReview.resolve(loaded.frames).first)
        var context = try PairContext(document: loaded, pair: pair)
        context.subjectConfirmed = true; context.referenceConfirmed = true
        context.comparability[.leftElbow] = .comparable
        loaded.comparisonContexts = [context]; loaded.revision += 1
        try ReviewStore.save(loaded, to: url, expectedRevision: 0)
        XCTAssertEqual(try Data(contentsOf: ReviewStore.legacyBackupURL(for: url)), bytes)
        let saved = try ReviewStore.load(from: url)
        XCTAssertEqual(saved.schemaVersion, 3)
        XCTAssertEqual(saved.comparisonContexts, [context])
        var changed = ReviewDocument(manifestHash: "changed", reportHash: "report", frames: loaded.frames)
        changed.comparisonContexts = [context]
        XCTAssertThrowsError(try PairReview.validate(changed))
    }
    func testBaselineRejectsRoleChangesAndAcceptsOrderChanges() throws {
        let baseline = pairDocument()
        var reordered = baseline; reordered.frames.reverse()
        XCTAssertNoThrow(try ReviewStore.validate(reordered, against: baseline))
        let swapped = ReviewFrame(id: "P01-reference", pairID: "other", role: "subject", rider: "reference", file: "reference.png", sourceHash: "reference", panel: baseline.frames[0].panel, raw3D: nil, raw2D: nil)
        var changed = baseline; changed.frames[0] = swapped
        XCTAssertThrowsError(try ReviewStore.validate(changed, against: baseline))
    }
}

extension PairReviewTests {
    func testBackupRetryMismatchSymlinkAndFutureSchemaPreserveSource() 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 legacy = pairDocument(); legacy.schemaVersion = 1
        let bytes = try JSONEncoder().encode(legacy)
        try bytes.write(to: url)
        let backup = ReviewStore.legacyBackupURL(for: url)
        try Data("wrong".utf8).write(to: backup)
        var next = legacy; next.revision = 1
        XCTAssertThrowsError(try ReviewStore.save(next, to: url, expectedRevision: 0))
        XCTAssertEqual(try Data(contentsOf: url), bytes)
        XCTAssertEqual(try Data(contentsOf: backup), Data("wrong".utf8))
        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)
        // Simulate successful backup followed by interrupted destination replacement.
        try bytes.write(to: backup)
        try ReviewStore.save(next, to: url, expectedRevision: 0)
        XCTAssertEqual(try ReviewStore.load(from: url).revision, 1)
        XCTAssertEqual(try Data(contentsOf: backup), bytes)
        XCTAssertThrowsError(try ReviewStore.save(legacy, to: url, expectedRevision: 0))
        var future = next; future.schemaVersion = 999
        let futureBytes = try JSONEncoder().encode(future); try futureBytes.write(to: url)
        XCTAssertThrowsError(try ReviewStore.save(next, to: url, expectedRevision: 1))
        XCTAssertEqual(try Data(contentsOf: url), futureBytes)
    }
    func testSchemaOneWithoutNewFieldAndAllContextStates() throws {
        var original = pairDocument()
        try original.frames[0].setJoint("left_elbow", point: Point2(x: 12, y: 14), state: .confirmed)
        original.revision = 5; original.schemaVersion = 1
        var json = try JSONSerialization.jsonObject(with: JSONEncoder().encode(original)) as! [String: Any]
        json.removeValue(forKey: "comparisonContexts")
        let legacy = try JSONDecoder().decode(ReviewDocument.self, from: JSONSerialization.data(withJSONObject: json))
        XCTAssertTrue(legacy.comparisonContexts.isEmpty)
        XCTAssertEqual(legacy.frames[0].edits, original.frames[0].edits)
        XCTAssertEqual(legacy.frames[0].history.count, 1)
        XCTAssertEqual(legacy.revision, 5)
        let pair = try XCTUnwrap(PairReview.resolve(original.frames).first)
        for corner in CornerDirection.allCases {
            var c = try PairContext(document: original, pair: pair); c.cornerDirection = corner
            c.comparability = [.leftElbow: .unreviewed, .rightElbow: .comparable, .leftKnee: .notComparable]
            XCTAssertEqual(try JSONDecoder().decode(PairContext.self, from: JSONEncoder().encode(c)), c)
        }
    }
}
