//
//  PostViewTests.swift
//  damusTests
//
//  Created by Daniel D’Aquino on 2023-08-19.
//
import Foundation
import XCTest
import SnapshotTesting
import SwiftUI
@testable import damus
import SwiftUI

final class PostViewTests: XCTestCase {
    
    override func setUpWithError() throws {
        // Put setup code here. This method is called before the invocation of each test method in the class.
    }
    
    override func tearDownWithError() throws {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
    }
    
    /*
    func testTextWrapperViewWillWrapText() {
        // Setup test variables to be passed into the TextViewWrapper
        let tagModel: TagModel = TagModel()
        var textHeight: CGFloat? = nil
        let textHeightBinding: Binding<CGFloat?> = Binding(get: {
            return textHeight
        }, set: { newValue in
            textHeight = newValue
        })
        
        // Setup the test view
        let textEditorView = TextViewWrapper(
            attributedText: .constant(NSMutableAttributedString(string: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")),
            textHeight: textHeightBinding,
            initialTextSuffix: nil,
            cursorIndex: 9,
            updateCursorPosition: { _ in return }
        ).environmentObject(tagModel)
        let hostView = UIHostingController(rootView: textEditorView)
        
        // Run snapshot check
        assertSnapshot(matching: hostView, as: .image(on: .iPhoneSe(.portrait)))
    }
     */

    /// Based on https://github.com/damus-io/damus/issues/1375
    /// Tests whether the editor properly handles mention links after they have been added, to avoid manual editing of attributed links
    func testMentionLinkEditorHandling() throws {
        var content: NSMutableAttributedString

        // Test normal insertion
        checkMentionLinkEditorHandling(content: NSMutableAttributedString(string: "Hello"), replacementText: "@", replacementRange: NSRange(location: 0, length: 0), shouldBeAbleToChangeAutomatically: true)
        checkMentionLinkEditorHandling(content: NSMutableAttributedString(string: "Hello "), replacementText: "@", replacementRange: NSRange(location: 6, length: 0), shouldBeAbleToChangeAutomatically: true)
        checkMentionLinkEditorHandling(content: NSMutableAttributedString(string: "Helo "), replacementText: "l", replacementRange: NSRange(location: 3, length: 0), shouldBeAbleToChangeAutomatically: true)
        
        // Test normal backspacing
        checkMentionLinkEditorHandling(content: NSMutableAttributedString(string: "Hello"), replacementText: "", replacementRange: NSRange(location: 5, length: 1), shouldBeAbleToChangeAutomatically: true)
        checkMentionLinkEditorHandling(content: NSMutableAttributedString(string: "Hello "), replacementText: "", replacementRange: NSRange(location: 6, length: 1), shouldBeAbleToChangeAutomatically: true)
        checkMentionLinkEditorHandling(content: NSMutableAttributedString(string: "Helo "), replacementText: "", replacementRange: NSRange(location: 3, length: 1), shouldBeAbleToChangeAutomatically: true)

        // Test normal insertion after mention link
        content = NSMutableAttributedString(string: "Hello @user ")
        content.addAttribute(.link, value: "damus:1234", range: NSRange(location: 6, length: 5))
        checkMentionLinkEditorHandling(content: content, replacementText: "a", replacementRange: NSRange(location: 12, length: 0), shouldBeAbleToChangeAutomatically: true)

        // Test insertion right at the end of a mention link, at the end of the text
        content = NSMutableAttributedString(string: "Hello @user")
        content.addAttribute(.link, value: "damus:1234", range: NSRange(location: 6, length: 5))
        checkMentionLinkEditorHandling(content: content, replacementText: ",", replacementRange: NSRange(location: 11, length: 0), shouldBeAbleToChangeAutomatically: false, expectedNewCursorIndex: 12, handleNewContent: { newManuallyEditedContent in
            XCTAssertEqual(newManuallyEditedContent.string, "Hello @user,")
            XCTAssertNil(newManuallyEditedContent.attribute(.link, at: 11, effectiveRange: nil))
        })

        // Test insertion right at the end of a mention link, in the middle of the text
        content = NSMutableAttributedString(string: "Hello @user how are you?")
        content.addAttribute(.link, value: "damus:1234", range: NSRange(location: 6, length: 5))
        checkMentionLinkEditorHandling(content: content, replacementText: ",", replacementRange: NSRange(location: 11, length: 0), shouldBeAbleToChangeAutomatically: false, expectedNewCursorIndex: 12, handleNewContent: { newManuallyEditedContent in
            XCTAssertEqual(newManuallyEditedContent.string, "Hello @user, how are you?")
            XCTAssertNil(newManuallyEditedContent.attribute(.link, at: 11, effectiveRange: nil))
        })

        // Test insertion in the middle of a mention link to check if the link is removed
        content = NSMutableAttributedString(string: "Hello @user how are you?")
        content.addAttribute(.link, value: "damus:1234", range: NSRange(location: 6, length: 5))
        checkMentionLinkEditorHandling(content: content, replacementText: "a", replacementRange: NSRange(location: 8, length: 0), shouldBeAbleToChangeAutomatically: false, expectedNewCursorIndex: 9, handleNewContent: { newManuallyEditedContent in
            XCTAssertEqual(newManuallyEditedContent.string, "Hello @uaser how are you?")
            XCTAssertNil(newManuallyEditedContent.attribute(.link, at: 8, effectiveRange: nil))
        })

        // Test insertion in the middle of a mention link to check if the link is removed, at the end of the text
        content = NSMutableAttributedString(string: "Hello @user")
        content.addAttribute(.link, value: "damus:1234", range: NSRange(location: 6, length: 5))
        checkMentionLinkEditorHandling(content: content, replacementText: "a", replacementRange: NSRange(location: 8, length: 0), shouldBeAbleToChangeAutomatically: false, expectedNewCursorIndex: 9, handleNewContent: { newManuallyEditedContent in
            XCTAssertEqual(newManuallyEditedContent.string, "Hello @uaser")
            XCTAssertNil(newManuallyEditedContent.attribute(.link, at: 8, effectiveRange: nil))
        })

        // Test backspacing right at the end of a mention link, at the end of the text
        content = NSMutableAttributedString(string: "Hello @user")
        content.addAttribute(.link, value: "damus:1234", range: NSRange(location: 6, length: 5))
        checkMentionLinkEditorHandling(content: content, replacementText: "", replacementRange: NSRange(location: 10, length: 1), shouldBeAbleToChangeAutomatically: false, expectedNewCursorIndex: 10, handleNewContent: { newManuallyEditedContent in
            XCTAssertEqual(newManuallyEditedContent.string, "Hello @use")
            XCTAssertNil(newManuallyEditedContent.attribute(.link, at: 6, effectiveRange: nil))
        })

        // Test adding text right at the start of a mention link - link should be preserved
        content = NSMutableAttributedString(string: "Hello @user")
        content.addAttribute(.link, value: "damus:1234", range: NSRange(location: 6, length: 5))
        checkMentionLinkEditorHandling(content: content, replacementText: "a", replacementRange: NSRange(location: 6, length: 0), shouldBeAbleToChangeAutomatically: false, expectedNewCursorIndex: 7, handleNewContent: { newManuallyEditedContent in
            XCTAssertEqual(newManuallyEditedContent.string, "Hello a@user")
            // Link should be preserved and shifted to position 7 (after the inserted "a")
            XCTAssertNotNil(newManuallyEditedContent.attribute(.link, at: 7, effectiveRange: nil))
        })

        // Test that removing one link does not affect the other
        content = NSMutableAttributedString(string: "Hello @user1 @user2")
        content.addAttribute(.link, value: "damus:1234", range: NSRange(location: 6, length: 6))
        content.addAttribute(.link, value: "damus:5678", range: NSRange(location: 13, length: 6))
        checkMentionLinkEditorHandling(content: content, replacementText: "", replacementRange: NSRange(location: 18, length: 1), shouldBeAbleToChangeAutomatically: false, expectedNewCursorIndex: 18, handleNewContent: { newManuallyEditedContent in
            XCTAssertEqual(newManuallyEditedContent.string, "Hello @user1 @user")
            XCTAssertNil(newManuallyEditedContent.attribute(.link, at: 13, effectiveRange: nil))
            XCTAssertNotNil(newManuallyEditedContent.attribute(.link, at: 6, effectiveRange: nil))
        })

        // Test that replacing a whole range intersecting with two links removes both links
        content = NSMutableAttributedString(string: "Hello @user1 @user2")
        content.addAttribute(.link, value: "damus:1234", range: NSRange(location: 6, length: 6))
        content.addAttribute(.link, value: "damus:5678", range: NSRange(location: 13, length: 6))
        checkMentionLinkEditorHandling(content: content, replacementText: "a", replacementRange: NSRange(location: 10, length: 4), shouldBeAbleToChangeAutomatically: false, expectedNewCursorIndex: 11, handleNewContent: { newManuallyEditedContent in
            XCTAssertEqual(newManuallyEditedContent.string, "Hello @useauser2")
            XCTAssertNil(newManuallyEditedContent.attribute(.link, at: 6, effectiveRange: nil))
            XCTAssertNil(newManuallyEditedContent.attribute(.link, at: 11, effectiveRange: nil))
        })

        // Test that replacing a whole range including two links removes both links naturally
        content = NSMutableAttributedString(string: "Hello @user1 @user2, how are you?")
        content.addAttribute(.link, value: "damus:1234", range: NSRange(location: 6, length: 6))
        content.addAttribute(.link, value: "damus:5678", range: NSRange(location: 13, length: 6))
        checkMentionLinkEditorHandling(content: content, replacementText: "", replacementRange: NSRange(location: 5, length: 28), shouldBeAbleToChangeAutomatically: true)
        
    }
    
    func testMentionLinkEditorHandling_noWhitespaceAfterLink1_addsWhitespace() {
        var content: NSMutableAttributedString

        content = NSMutableAttributedString(string: "Hello @user ")
        content.addAttribute(.link, value: "damus:1234", range: NSRange(location: 6, length: 5))
        checkMentionLinkEditorHandling(content: content, replacementText: "up", replacementRange: NSRange(location: 11, length: 1), shouldBeAbleToChangeAutomatically: true, expectedNewCursorIndex: 13, handleNewContent: { newManuallyEditedContent in
            XCTAssertEqual(newManuallyEditedContent.string, "Hello @user up")
            XCTAssertNil(newManuallyEditedContent.attribute(.link, at: 11, effectiveRange: nil))
        })
    }
    
    func testMentionLinkEditorHandling_noWhitespaceAfterLink2_addsWhitespace() {
        var content: NSMutableAttributedString

        content = NSMutableAttributedString(string: "Hello @user test")
        content.addAttribute(.link, value: "damus:1234", range: NSRange(location: 6, length: 5))
        checkMentionLinkEditorHandling(content: content, replacementText: "up", replacementRange: NSRange(location: 11, length: 1), shouldBeAbleToChangeAutomatically: true, expectedNewCursorIndex: 13, handleNewContent: { newManuallyEditedContent in
            XCTAssertEqual(newManuallyEditedContent.string, "Hello @user uptest")
            XCTAssertNil(newManuallyEditedContent.attribute(.link, at: 11, effectiveRange: nil))
        })
    }
    
    func testMentionLinkEditorHandling_nonAlphanumericAfterLink_noWhitespaceAdded() {
        let nonAlphaNumerics = [" ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", ":", ";", "<", "=", ">", "?", "@", "[", "\\", "]", "^", "_", "`", "{", "|", "}", "~"]
        
        nonAlphaNumerics.forEach { testAddingStringAfterLink(str: $0)}
    }

    func testQuoteRepost() async {
        let post = await build_post(state: test_damus_state, post: .init(), action: .quoting(test_note), uploadedMedias: [], pubkeys: [])

        XCTAssertEqual(post.tags, [["q", test_note.id.hex(), "", jack_keypair.pubkey.hex()], ["p", jack_keypair.pubkey.hex()]])
    }

    func testBuildPostRecognizesStringsAsNpubs() async throws {
        // given
        let expectedLink = "nostr:\(test_pubkey.npub)"
        let content = NSMutableAttributedString(string: "@test", attributes: [
            NSAttributedString.Key.link: "damus:\(expectedLink)"
        ])

        // when
        let post = await build_post(
            state: test_damus_state,
            post: content,
            action: .posting(.user(test_pubkey)),
            uploadedMedias: [],
            pubkeys: []
        )

        // then
        XCTAssertEqual(post.content, expectedLink)
    }

    func testBuildPostRecognizesUrlsAsNpubs() async throws {
        // given
        guard let npubUrl = URL(string: "damus:nostr:\(test_pubkey.npub)") else {
            return XCTFail("Could not create URL")
        }
        let content = NSMutableAttributedString(string: "@test", attributes: [
            NSAttributedString.Key.link: npubUrl
        ])

        // when
        let post = await build_post(
            state: test_damus_state,
            post: content,
            action: .posting(.user(test_pubkey)),
            uploadedMedias: [],
            pubkeys: []
        )

        // then
        XCTAssertEqual(post.content, "nostr:\(test_pubkey.npub)")
    }

/// Tests that pasting an npub converts it to a mention link (issue #2289)
    func testPastedNpubConvertsToMention() {
        let content = NSMutableAttributedString(string: "Hello ")
        var resultContent: NSMutableAttributedString?

        let bindingContent: Binding<NSMutableAttributedString> = Binding(get: {
            return content
        }, set: { newValue in
            resultContent = newValue
        })

        let coordinator = TextViewWrapper.Coordinator(
            attributedText: bindingContent,
            getFocusWordForMention: nil,
            updateCursorPosition: { _ in },
            initialTextSuffix: nil,
            convertMentionRef: { pubkey in
                // Return a mock mention tag
                return NSMutableAttributedString(string: "@testuser", attributes: [
                    NSAttributedString.Key.link: "damus:nostr:\(pubkey.npub)"
                ])
            }
        )

        let textView = UITextView()
        textView.attributedText = content

        // Paste an npub - should return false (handled manually) and convert to mention
        let npub = test_pubkey.npub
        let shouldChange = coordinator.textView(textView, shouldChangeTextIn: NSRange(location: 6, length: 0), replacementText: npub)

        XCTAssertFalse(shouldChange, "shouldChangeTextIn should return false when converting npub to mention")
        XCTAssertNotNil(resultContent, "Content should be updated with mention")
        XCTAssertTrue(resultContent?.string.contains("@testuser") ?? false, "Content should contain the mention username")
    }

    /// Tests that pasting an nprofile converts it to a mention link (issue #2289)
    func testPastedNprofileConvertsToMention() {
        let content = NSMutableAttributedString(string: "")
        var resultContent: NSMutableAttributedString?

        let bindingContent: Binding<NSMutableAttributedString> = Binding(get: {
            return content
        }, set: { newValue in
            resultContent = newValue
        })

        let coordinator = TextViewWrapper.Coordinator(
            attributedText: bindingContent,
            getFocusWordForMention: nil,
            updateCursorPosition: { _ in },
            initialTextSuffix: nil,
            convertMentionRef: { pubkey in
                return NSMutableAttributedString(string: "@profileuser", attributes: [
                    NSAttributedString.Key.link: "damus:nostr:\(pubkey.npub)"
                ])
            }
        )

        let textView = UITextView()
        textView.attributedText = content

        // Create a valid nprofile for test_pubkey
        let nprofile = Bech32Object.encode(.nprofile(NProfile(author: test_pubkey, relays: [])))
        let shouldChange = coordinator.textView(textView, shouldChangeTextIn: NSRange(location: 0, length: 0), replacementText: nprofile)

        XCTAssertFalse(shouldChange, "shouldChangeTextIn should return false when converting nprofile to mention")
        XCTAssertNotNil(resultContent, "Content should be updated with mention")
        XCTAssertTrue(resultContent?.string.contains("@profileuser") ?? false, "Content should contain the mention username")
    }

    /// Tests that regular text is not converted (not npub/nprofile)
    func testRegularTextNotConverted() {
        let content = NSMutableAttributedString(string: "Hello ")

        let bindingContent: Binding<NSMutableAttributedString> = Binding(get: {
            return content
        }, set: { _ in })

        let coordinator = TextViewWrapper.Coordinator(
            attributedText: bindingContent,
            getFocusWordForMention: nil,
            updateCursorPosition: { _ in },
            initialTextSuffix: nil,
            convertMentionRef: { _ in
                XCTFail("convertMentionRef should not be called for regular text")
                return nil
            }
        )

        let textView = UITextView()
        textView.attributedText = content

        // Paste regular text - should return true (not handled, allow default behavior)
        let shouldChange = coordinator.textView(textView, shouldChangeTextIn: NSRange(location: 6, length: 0), replacementText: "world")

        XCTAssertTrue(shouldChange, "shouldChangeTextIn should return true for regular text")
    }

    /// Tests that client tags are added to events when provided.
    func testToEventAddsClientTagWhenProvided() {
        let post = NostrPost(content: "gm")
        let clientTag = ["client", "Damus"]
        let event = post.to_event(keypair: test_keypair_full, clientTag: clientTag)
        XCTAssertTrue(event?.tags.contains(where: { $0.strings() == clientTag }) ?? false)
    }

    /// Tests that existing client tags are not duplicated.
    func testToEventDoesNotDuplicateExistingClientTag() {
        let existingTags = [["client", "Custom"]]
        let post = NostrPost(content: "gm", tags: existingTags)
        let clientTag = ["client", "Damus"]
        let event = post.to_event(keypair: test_keypair_full, clientTag: clientTag)
        let clientTagCount = event?.tags.filter { $0[0].matches_str("client") }.count
        XCTAssertEqual(clientTagCount, 1)
        XCTAssertEqual(event?.tags.first(where: { $0[0].matches_str("client") })?.strings(), existingTags.first)
    }
}

func checkMentionLinkEditorHandling(
    content: NSMutableAttributedString,
    replacementText: String,
    replacementRange: NSRange,
    shouldBeAbleToChangeAutomatically: Bool,
    expectedNewCursorIndex: Int? = nil,
    handleNewContent: ((NSMutableAttributedString) -> Void)? = nil) {
        let bindingContent: Binding<NSMutableAttributedString> = Binding(get: {
            return content
        }, set: { newValue in
            handleNewContent?(newValue)
        })
        let coordinator: TextViewWrapper.Coordinator = TextViewWrapper.Coordinator(attributedText: bindingContent, getFocusWordForMention: nil, updateCursorPosition: { newCursorIndex in
            if let expectedNewCursorIndex {
                XCTAssertEqual(newCursorIndex, expectedNewCursorIndex)
            }
        }, initialTextSuffix: nil, convertMentionRef: nil)
        let textView = UITextView()
        textView.attributedText = content

        XCTAssertEqual(coordinator.textView(textView, shouldChangeTextIn: replacementRange, replacementText: replacementText), shouldBeAbleToChangeAutomatically, "Expected shouldChangeTextIn to return \(shouldBeAbleToChangeAutomatically), but was \(!shouldBeAbleToChangeAutomatically)")
}

func testAddingStringAfterLink(str: String) {
    var content: NSMutableAttributedString

    content = NSMutableAttributedString(string: "Hello @user test")
    content.addAttribute(.link, value: "damus:1234", range: NSRange(location: 6, length: 5))
    checkMentionLinkEditorHandling(content: content, replacementText: str, replacementRange: NSRange(location: 11, length: 0), shouldBeAbleToChangeAutomatically: false, expectedNewCursorIndex: 12, handleNewContent: { newManuallyEditedContent in
        XCTAssertEqual(newManuallyEditedContent.string, "Hello @user" + str + " test")
        XCTAssertNil(newManuallyEditedContent.attribute(.link, at: 11, effectiveRange: nil))
    })
}

