import ForgejoKit
import Foundation
import Testing
@testable import Forji

private typealias FIssue = ForgejoKit.Issue

struct TaggedItemTests {

    private func makeAuth() -> AuthenticationService {
        AuthenticationService()
    }

    private func makeIssue(id: Int, title: String = "Test", updatedAt: Date = Date()) -> FIssue {
        FIssue(
            id: id, number: id, title: title, state: "open",
            user: User(id: 1, login: "test", avatarUrl: nil),
            createdAt: Date(), updatedAt: updatedAt
        )
    }

    private func makeSource(
        key: String = "https://example.com:user",
        name: String = "Example",
    ) -> ConnectionSource {
        ConnectionSource(
            sourceKey: key,
            name: name,
            client: ForgejoClient(serverURL: "https://example.com", username: "user", token: "t"),
            authService: makeAuth()
        )
    }

    // MARK: - ID Generation

    @Test func idUsesSourceKeyNotDisplayName() {
        let auth = makeAuth()
        let issue = makeIssue(id: 42)
        let tagged = TaggedItem(
            item: issue, sourceKey: "https://work.com:admin",
            instanceName: "Work", authService: auth
        )
        #expect(tagged.id == "https://work.com:admin:42")
        #expect(tagged.instanceName == "Work")
    }

    @Test func sameItemDifferentSourceKeysHaveDifferentIds() {
        let auth = makeAuth()
        let issue = makeIssue(id: 1)
        let taggedA = TaggedItem(
            item: issue, sourceKey: "https://a.com:user",
            instanceName: "A", authService: auth
        )
        let taggedB = TaggedItem(
            item: issue, sourceKey: "https://b.com:user",
            instanceName: "B", authService: auth
        )
        #expect(taggedA.id != taggedB.id)
    }

    @Test func sameDisplayNameDifferentSourceKeysNeverCollide() {
        let auth = makeAuth()
        let issue = makeIssue(id: 1)
        let taggedA = TaggedItem(
            item: issue, sourceKey: "https://a.com:user",
            instanceName: "Same Name", authService: auth
        )
        let taggedB = TaggedItem(
            item: issue, sourceKey: "https://b.com:user",
            instanceName: "Same Name", authService: auth
        )
        #expect(taggedA.id != taggedB.id)
    }

    @Test func sameSourceKeySameItemIdProducesSameId() {
        let auth = makeAuth()
        let issueA = makeIssue(id: 5)
        let issueB = makeIssue(id: 5, title: "Different title")
        let taggedA = TaggedItem(
            item: issueA, sourceKey: "srv", instanceName: "srv", authService: auth
        )
        let taggedB = TaggedItem(
            item: issueB, sourceKey: "srv", instanceName: "srv", authService: auth
        )
        #expect(taggedA.id == taggedB.id)
    }

    // MARK: - Merge and Deduplicate

    @Test func mergeAndDeduplicateRemovesDuplicateIds() {
        let source = makeSource()
        let issue = makeIssue(id: 1)
        let batches: [(Int, [FIssue])] = [(0, [issue]), (0, [issue])]

        let result = TaggedItem.mergeAndDeduplicate(batches: batches, sources: [source])
        #expect(result.count == 1)
    }

    @Test func mergeAndDeduplicateKeepsItemsFromDifferentSources() {
        let sourceA = makeSource(key: "https://work.com:u", name: "work")
        let sourceB = makeSource(key: "https://personal.com:u", name: "personal")
        let issue = makeIssue(id: 1)
        let batches: [(Int, [FIssue])] = [(0, [issue]), (1, [issue])]

        let result = TaggedItem.mergeAndDeduplicate(batches: batches, sources: [sourceA, sourceB])
        #expect(result.count == 2)
    }

    @Test func mergeAndDeduplicatePreservesAllFields() {
        let source = makeSource(key: "https://example.com:u", name: "myserver")
        let issue = makeIssue(id: 7, title: "Important")
        let batches: [(Int, [FIssue])] = [(0, [issue])]

        let result = TaggedItem.mergeAndDeduplicate(batches: batches, sources: [source])
        #expect(result.count == 1)
        #expect(result[0].item.title == "Important")
        #expect(result[0].instanceName == "myserver")
        #expect(result[0].id == "https://example.com:u:7")
    }

    @Test func mergeAndDeduplicateReturnsEmptyForEmptyBatches() {
        let result: [TaggedItem<FIssue>] = TaggedItem.mergeAndDeduplicate(batches: [], sources: [])
        #expect(result.isEmpty)
    }

    // MARK: - Codable Round-Trip

    @Test func codableRoundTripPreservesFields() throws {
        let issue = makeIssue(id: 3, title: "Cached")
        let tagged = TaggedItem(
            item: issue, sourceKey: "https://srv.com:admin",
            instanceName: "Server", authService: makeAuth()
        )

        let data = try JSONEncoder().encode([tagged])
        let decoded = try JSONDecoder().decode([TaggedItem<FIssue>].self, from: data)

        #expect(decoded.count == 1)
        #expect(decoded[0].id == "https://srv.com:admin:3")
        #expect(decoded[0].sourceKey == "https://srv.com:admin")
        #expect(decoded[0].item.title == "Cached")
        #expect(decoded[0].instanceName == "Server")
    }

    @Test func codableDecodedAuthServiceIsPlaceholder() throws {
        let issue = makeIssue(id: 1)
        let tagged = TaggedItem(
            item: issue, sourceKey: "key",
            instanceName: "name", authService: makeAuth()
        )

        let data = try JSONEncoder().encode([tagged])
        let decoded = try JSONDecoder().decode([TaggedItem<FIssue>].self, from: data)

        #expect(decoded[0].authService.isAuthenticated == false)
        #expect(decoded[0].authService.client == nil)
    }

    // MARK: - Rehydrate

    @MainActor
    @Test func rehydrateReplacesAuthService() {
        let liveAuth = AuthenticationService()
        let source = ConnectionSource(
            sourceKey: "https://a.com:u", name: "A",
            client: ForgejoClient(serverURL: "https://a.com", username: "u", token: "t"),
            authService: liveAuth
        )
        let issue = makeIssue(id: 1)
        let pagination = PaginationState<TaggedItem<FIssue>>()
        pagination.items = [
            TaggedItem(item: issue, sourceKey: "https://a.com:u", instanceName: "A", authService: makeAuth()),
        ]

        pagination.rehydrate(from: [source])

        #expect(pagination.items.count == 1)
        #expect(pagination.items[0].authService === liveAuth)
    }

    @MainActor
    @Test func rehydrateRemovesItemsWithNoMatchingSource() {
        let issue = makeIssue(id: 1)
        let pagination = PaginationState<TaggedItem<FIssue>>()
        pagination.items = [
            TaggedItem(item: issue, sourceKey: "https://removed.com:u", instanceName: "Gone", authService: makeAuth()),
        ]

        pagination.rehydrate(from: [ConnectionSource]())

        #expect(pagination.items.isEmpty)
    }

    @MainActor
    @Test func rehydrateHandlesDuplicateSourceKeys() {
        // Two connected accounts with the same server+username produce duplicate
        // sourceKeys; rehydrate must not trap on the duplicate key.
        let liveAuth = AuthenticationService()
        let key = "https://a.com:u"
        let sources = [
            ConnectionSource(
                sourceKey: key, name: "A",
                client: ForgejoClient(serverURL: "https://a.com", username: "u", token: "t"),
                authService: liveAuth,
            ),
            ConnectionSource(
                sourceKey: key, name: "A (duplicate)",
                client: ForgejoClient(serverURL: "https://a.com", username: "u", token: "t"),
                authService: AuthenticationService(),
            ),
        ]
        let pagination = PaginationState<TaggedItem<FIssue>>()
        pagination.items = [
            TaggedItem(item: makeIssue(id: 1), sourceKey: key, instanceName: "A", authService: makeAuth()),
        ]

        pagination.rehydrate(from: sources)

        #expect(pagination.items.count == 1)
        #expect(pagination.items[0].authService === liveAuth) // first source wins
    }
}
