import AppKit
@testable import CLI
@testable import Core
import CoreProtocols
import Crawler
import CrawlerModels
import Foundation
import LoggingModels
@testable import Search
import SearchModels
import SharedConstants
import Testing
import TestSupport

// MARK: - Test Doubles

private struct NoopMarkdownStrategy: Search.MarkdownToStructuredPageStrategy {
    func convert(markdown: String, url: URL?) -> Shared.Models.StructuredDocumentationPage? {
        nil
    }
}

private struct MissingSampleCatalogProvider: Search.SampleCatalogProvider {
    func fetch() async -> Search.SampleCatalogState {
        .missing(onDiskPath: "")
    }
}

// MARK: - Save Command Tests

// Tests for the `cupertino save` command
// Verifies search index building, framework filtering, and empty directory handling

@Suite("Save Command Tests", .serialized)
struct SaveCommandTests {
    @Test("Build search index from crawled docs", .tags(.integration))
    @MainActor
    func buildSearchIndex() async throws {
        _ = NSApplication.shared

        let tempDir = FileManager.default.temporaryDirectory
            .appendingPathComponent("cupertino-save-test-\(UUID().uuidString)")
        defer { try? FileManager.default.removeItem(at: tempDir) }

        print("🧪 Test: Build search index")

        // First, fetch a page to have data
        let config = try Shared.Configuration(
            crawler: Shared.Configuration.Crawler(
                startURL: #require(URL(string: "https://developer.apple.com/documentation/swift")),
                maxPages: 1,
                maxDepth: 0,
                outputDirectory: tempDir
            ),
            changeDetection: Shared.Configuration.ChangeDetection(forceRecrawl: true, outputDirectory: tempDir),
            output: Shared.Configuration.Output(format: .markdown)
        )

        let crawler = await Crawler.AppleDocs(
            configuration: config,
            htmlParser: LiveTestHTMLParserStrategy(),
            appleJSONParser: LiveTestAppleJSONParserStrategy(),
            priorityPackageStrategy: LiveTestPriorityPackageStrategy(),
            logger: Logging.NoopRecording()
        )
        _ = try await crawler.crawl()

        // Build search index
        let searchDbPath = tempDir.appendingPathComponent("search.db")
        let searchIndex = try await Search.Index(dbPath: searchDbPath, logger: Logging.NoopRecording())

        let metadata = try Shared.Models.CrawlMetadata.load(from: tempDir.appendingPathComponent("metadata.json"))
        let builder = Search.IndexBuilder(
            searchIndex: searchIndex,
            metadata: metadata,
            docsDirectory: tempDir,
            evolutionDirectory: nil,
            markdownStrategy: NoopMarkdownStrategy(),
            sampleCatalogProvider: MissingSampleCatalogProvider(), logger: Logging.NoopRecording()
        )

        try await builder.buildIndex()

        // Verify search.db was created
        #expect(FileManager.default.fileExists(atPath: searchDbPath.path), "Search database should exist")

        // Verify we can search
        let results = try await searchIndex.search(query: "swift", limit: 10)
        #expect(!results.isEmpty, "Search should return results")

        print("   ✅ Found \(results.count) search results")
        if let firstResult = results.first {
            print("   ✅ First result: \(firstResult.title)")
        }

        print("   ✅ Save build test passed!")
    }

    @Test("Search index with framework filter", .tags(.integration))
    @MainActor
    func searchWithFrameworkFilter() async throws {
        _ = NSApplication.shared

        let tempDir = FileManager.default.temporaryDirectory
            .appendingPathComponent("cupertino-search-test-\(UUID().uuidString)")
        defer { try? FileManager.default.removeItem(at: tempDir) }

        print("🧪 Test: Search with framework filter")

        // Fetch and save
        let config = try Shared.Configuration(
            crawler: Shared.Configuration.Crawler(
                startURL: #require(URL(string: "https://developer.apple.com/documentation/swift")),
                maxPages: 1,
                maxDepth: 0,
                outputDirectory: tempDir
            ),
            changeDetection: Shared.Configuration.ChangeDetection(forceRecrawl: true, outputDirectory: tempDir),
            output: Shared.Configuration.Output(format: .markdown)
        )

        let crawler = await Crawler.AppleDocs(
            configuration: config,
            htmlParser: LiveTestHTMLParserStrategy(),
            appleJSONParser: LiveTestAppleJSONParserStrategy(),
            priorityPackageStrategy: LiveTestPriorityPackageStrategy(),
            logger: Logging.NoopRecording()
        )
        _ = try await crawler.crawl()

        let searchDbPath = tempDir.appendingPathComponent("search.db")
        let searchIndex = try await Search.Index(dbPath: searchDbPath, logger: Logging.NoopRecording())

        let metadata = try Shared.Models.CrawlMetadata.load(from: tempDir.appendingPathComponent("metadata.json"))
        let builder = Search.IndexBuilder(
            searchIndex: searchIndex,
            metadata: metadata,
            docsDirectory: tempDir,
            evolutionDirectory: nil,
            markdownStrategy: NoopMarkdownStrategy(),
            sampleCatalogProvider: MissingSampleCatalogProvider(), logger: Logging.NoopRecording()
        )
        try await builder.buildIndex()

        // Search with framework filter
        let allResults = try await searchIndex.search(query: "array", limit: 10)
        let swiftResults = try await searchIndex.search(query: "array", framework: "swift", limit: 10)

        #expect(!allResults.isEmpty, "General search should return results")

        if !swiftResults.isEmpty {
            for result in swiftResults {
                #expect(result.framework == "swift", "Filtered results should match framework")
            }
            print("   ✅ Framework filter working correctly")
        }

        print("   ✅ Search filter test passed!")
    }

    @Test("Save handles empty directory gracefully")
    func saveEmptyDirectory() async throws {
        let tempDir = FileManager.default.temporaryDirectory
            .appendingPathComponent("cupertino-empty-save-test-\(UUID().uuidString)")
        defer { try? FileManager.default.removeItem(at: tempDir) }

        try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)

        print("🧪 Test: Save empty directory")

        let searchDbPath = tempDir.appendingPathComponent("search.db")
        let searchIndex = try await Search.Index(dbPath: searchDbPath, logger: Logging.NoopRecording())

        // Create empty metadata
        let emptyMetadata = Shared.Models.CrawlMetadata()
        let metadataFile = tempDir.appendingPathComponent("metadata.json")
        try emptyMetadata.save(to: metadataFile)

        let builder = Search.IndexBuilder(
            searchIndex: searchIndex,
            metadata: emptyMetadata,
            docsDirectory: tempDir,
            evolutionDirectory: nil,
            markdownStrategy: NoopMarkdownStrategy(),
            sampleCatalogProvider: MissingSampleCatalogProvider(), logger: Logging.NoopRecording()
        )

        // Should not throw, just save 0 documents
        try await builder.buildIndex()

        let results = try await searchIndex.search(query: "anything", limit: 10)
        #expect(results.isEmpty, "Empty save should return no results")

        print("   ✅ Empty directory test passed!")
    }

    @Test("Base directory auto-fills all paths")
    func baseDirAutoFillsPaths() async throws {
        let baseDir = FileManager.default.temporaryDirectory
            .appendingPathComponent("cupertino-basedir-test-\(UUID().uuidString)")
        defer { try? FileManager.default.removeItem(at: baseDir) }

        print("🧪 Test: Base directory auto-fills paths")

        // Create directory structure
        let docsDir = baseDir.appendingPathComponent("docs")
        let evolutionDir = baseDir.appendingPathComponent("swift-evolution")
        let searchDbPath = baseDir.appendingPathComponent("search.db")

        try FileManager.default.createDirectory(at: docsDir, withIntermediateDirectories: true)
        try FileManager.default.createDirectory(at: evolutionDir, withIntermediateDirectories: true)

        // Create minimal metadata
        let metadata = Shared.Models.CrawlMetadata()
        let metadataFile = baseDir.appendingPathComponent("metadata.json")
        try metadata.save(to: metadataFile)

        // Verify paths derived from base directory
        #expect(
            docsDir.path == baseDir.appendingPathComponent(Shared.Constants.Directory.docs).path,
            "docs-dir should be base-dir/docs"
        )
        #expect(
            evolutionDir.path == baseDir.appendingPathComponent(Shared.Constants.Directory.swiftEvolution).path,
            "evolution-dir should be base-dir/swift-evolution"
        )
        #expect(
            metadataFile.path == baseDir.appendingPathComponent(Shared.Constants.FileName.metadata).path,
            "metadata-file should be base-dir/metadata.json"
        )
        #expect(
            searchDbPath.path == baseDir.appendingPathComponent(Shared.Constants.FileName.searchDatabase).path,
            "search-db should be base-dir/search.db"
        )

        print("   ✅ Base directory paths verified!")

        // Test that index builds successfully with base-dir structure
        let searchIndex = try await Search.Index(dbPath: searchDbPath, logger: Logging.NoopRecording())
        let builder = Search.IndexBuilder(
            searchIndex: searchIndex,
            metadata: metadata,
            docsDirectory: docsDir,
            evolutionDirectory: evolutionDir,
            markdownStrategy: NoopMarkdownStrategy(),
            sampleCatalogProvider: MissingSampleCatalogProvider(), logger: Logging.NoopRecording()
        )

        try await builder.buildIndex()

        #expect(FileManager.default.fileExists(atPath: searchDbPath.path), "Search DB should be created")

        print("   ✅ Base directory test passed!")
    }

    // MARK: - Directory Scanning Tests (metadata.json optional)

    @Test("Build index without metadata.json using directory scanning")
    func buildIndexWithoutMetadata() async throws {
        let tempDir = FileManager.default.temporaryDirectory
            .appendingPathComponent("cupertino-no-metadata-test-\(UUID().uuidString)")
        defer { try? FileManager.default.removeItem(at: tempDir) }

        print("🧪 Test: Build index without metadata.json")

        // Create directory structure: docs/swift/array.json
        let swiftDir = tempDir.appendingPathComponent("docs/swift")
        try FileManager.default.createDirectory(at: swiftDir, withIntermediateDirectories: true)

        // Create test JSON files (StructuredDocumentationPage format)
        let arrayPage = try Shared.Models.StructuredDocumentationPage(
            url: #require(URL(string: "https://developer.apple.com/documentation/swift/array")),
            title: "Array",
            kind: .struct,
            source: .appleWebKit,
            abstract: "An ordered collection of elements.",
            rawMarkdown: "# Array\n\nAn ordered collection of elements."
        )
        let arrayDoc = swiftDir.appendingPathComponent("array.json")
        try Shared.Utils.JSONCoding.encode(arrayPage, to: arrayDoc)

        let dictPage = try Shared.Models.StructuredDocumentationPage(
            url: #require(URL(string: "https://developer.apple.com/documentation/swift/dictionary")),
            title: "Dictionary",
            kind: .struct,
            source: .appleWebKit,
            abstract: "A collection of key-value pairs.",
            rawMarkdown: "# Dictionary\n\nA collection of key-value pairs."
        )
        let dictDoc = swiftDir.appendingPathComponent("dictionary.json")
        try Shared.Utils.JSONCoding.encode(dictPage, to: dictDoc)

        // Create swiftui directory
        let swiftuiDir = tempDir.appendingPathComponent("docs/swiftui")
        try FileManager.default.createDirectory(at: swiftuiDir, withIntermediateDirectories: true)

        let viewPage = try Shared.Models.StructuredDocumentationPage(
            url: #require(URL(string: "https://developer.apple.com/documentation/swiftui/view")),
            title: "View",
            kind: .protocol,
            source: .appleWebKit,
            abstract: "A piece of user interface.",
            rawMarkdown: "# View\n\nA piece of user interface."
        )
        let viewDoc = swiftuiDir.appendingPathComponent("view.json")
        try Shared.Utils.JSONCoding.encode(viewPage, to: viewDoc)

        // Build index WITHOUT metadata.json
        let searchDbPath = tempDir.appendingPathComponent("search.db")
        let searchIndex = try await Search.Index(dbPath: searchDbPath, logger: Logging.NoopRecording())

        let builder = Search.IndexBuilder(
            searchIndex: searchIndex,
            metadata: nil, // No metadata!
            docsDirectory: tempDir.appendingPathComponent("docs"),
            evolutionDirectory: nil,
            markdownStrategy: NoopMarkdownStrategy(),
            sampleCatalogProvider: MissingSampleCatalogProvider(), logger: Logging.NoopRecording()
        )

        try await builder.buildIndex()

        // Verify documents were indexed
        let swiftResults = try await searchIndex.search(query: "collection", framework: "swift", limit: 10)
        #expect(!swiftResults.isEmpty, "Should find swift documents")
        #expect(swiftResults.contains(where: { $0.title.contains("Array") }), "Should index Array")

        let swiftuiResults = try await searchIndex.search(query: "interface", framework: "swiftui", limit: 10)
        #expect(!swiftuiResults.isEmpty, "Should find swiftui documents")

        // Verify framework extraction from folder path
        let allResults = try await searchIndex.search(query: "collection OR interface", limit: 10)
        for result in allResults {
            #expect(["swift", "swiftui"].contains(result.framework), "Framework should be extracted from path")
        }

        print("   ✅ Indexed \(allResults.count) documents without metadata.json")
        print("   ✅ Directory scanning test passed!")
    }

    @Test("Directory scanning handles nested framework folders")
    func directoryScanningNestedFolders() async throws {
        let tempDir = FileManager.default.temporaryDirectory
            .appendingPathComponent("cupertino-nested-test-\(UUID().uuidString)")
        defer { try? FileManager.default.removeItem(at: tempDir) }

        print("🧪 Test: Directory scanning with nested folders")

        // Create nested structure: docs/foundation/collections/array.json
        let nestedDir = tempDir.appendingPathComponent("docs/foundation/collections")
        try FileManager.default.createDirectory(at: nestedDir, withIntermediateDirectories: true)

        let nestedPage = try Shared.Models.StructuredDocumentationPage(
            url: #require(URL(string: "https://developer.apple.com/documentation/foundation/nsarray")),
            title: "NSArray",
            kind: .class,
            source: .appleWebKit,
            abstract: "Foundation array class.",
            rawMarkdown: "# NSArray\n\nFoundation array class."
        )
        let nestedDoc = nestedDir.appendingPathComponent("array.json")
        try Shared.Utils.JSONCoding.encode(nestedPage, to: nestedDoc)

        // Build index
        let searchDbPath = tempDir.appendingPathComponent("search.db")
        let searchIndex = try await Search.Index(dbPath: searchDbPath, logger: Logging.NoopRecording())

        let builder = Search.IndexBuilder(
            searchIndex: searchIndex,
            metadata: nil,
            docsDirectory: tempDir.appendingPathComponent("docs"),
            evolutionDirectory: nil,
            markdownStrategy: NoopMarkdownStrategy(),
            sampleCatalogProvider: MissingSampleCatalogProvider(), logger: Logging.NoopRecording()
        )

        try await builder.buildIndex()

        // Verify framework is "foundation" (first folder after docs/)
        let results = try await searchIndex.search(query: "array", limit: 10)
        #expect(!results.isEmpty, "Should find nested documents")
        #expect(results.first?.framework == "foundation", "Should extract framework from first subfolder")

        print("   ✅ Nested folder test passed!")
    }

    // MARK: - Sample Code Catalog Tests

    // The two sample-code SaveTests cases that lived here previously
    // ("Index sample code catalog from bundled resources" + "Sample code
    // catalog respects framework filter") assumed `Sample.Core.Catalog`
    // always returned ~600 entries from the embedded blob. After #215
    // deleted that blob, the catalog only exists when
    // `<sample-code-dir>/catalog.json` is present, so a CI machine with
    // no fetched data sees 0 entries and the tests fail.
    //
    // Replacement coverage:
    //  - Disk-fixture loading + format invariants:
    //    `Tests/CoreTests/SampleCodeCatalogTests.swift`
    //  - Save→search-of-sample-code integration: deferred until we have a
    //    test seam for injecting a sample-code dir into `Sample.Core.Catalog`
    //    (the cached load currently reads from
    //    `Shared.Constants.defaultSampleCodeDirectory` directly, so a test
    //    can't sandbox without polluting user data).

    // MARK: - Package Catalog Tests

    // Post-#194: the 568 KB embedded swift-packages URL catalog was
    // deleted in favour of the `packages.db` artifact distributed via
    // `cupertino setup`. `Core.Protocols.SwiftPackagesCatalog.allPackages`
    // now returns []; `Search.Strategies.SwiftPackages` hits its
    // existing `guard !packages.isEmpty` clean-skip path (#671). These
    // tests pin the post-#194 empty contract through the full
    // Search.IndexBuilder path. Restored coverage against packages.db
    // fixtures lives in PackageQueryTests and is out of scope for #194.

    @Test("Index Swift packages catalog: empty post-#194; strategy clean-skips")
    func indexPackagesCatalogIsEmpty() async throws {
        let tempDir = FileManager.default.temporaryDirectory
            .appendingPathComponent("cupertino-packages-test-\(UUID().uuidString)")
        defer { try? FileManager.default.removeItem(at: tempDir) }

        print("🧪 Test: Index packages catalog (post-#194 empty contract)")

        let searchDbPath = tempDir.appendingPathComponent("search.db")
        let searchIndex = try await Search.Index(dbPath: searchDbPath, logger: Logging.NoopRecording())

        let builder = Search.IndexBuilder(
            searchIndex: searchIndex,
            metadata: nil,
            docsDirectory: tempDir,
            evolutionDirectory: nil,
            markdownStrategy: NoopMarkdownStrategy(),
            sampleCatalogProvider: MissingSampleCatalogProvider(), logger: Logging.NoopRecording()
        )

        try await builder.buildIndex()

        // Post-#789 contract: the search.db `packages` and
        // `package_dependencies` tables have been removed entirely.
        // The SwiftPackagesStrategy was removed; packages live in
        // packages.db, queried via `cupertino package-search`. The
        // per-source breakdown for the docs build no longer carries
        // a 'swift-packages' entry; assert via document count instead
        // (apple-docs phase is the only source in this minimal builder).
        let docCount = try await searchIndex.documentCount()
        #expect(docCount >= 0, "post-#789: doc count is the readable invariant; got \(docCount)")

        print("   ✅ Empty-contract pinned (#789)")
    }

    @Test("Package catalog metadata: accessor returns [] post-#194")
    func packageCatalogIsEmpty() async {
        // Direct accessor pin — independent of the builder path above.
        // `Core.Protocols.SwiftPackagesCatalog.allPackages` is the
        // upstream source the indexer reads; this test guards against
        // a future revert re-introducing the embedded literal without
        // also revisiting the indexer / packages.db pipeline.
        let entries = await Core.Protocols.SwiftPackagesCatalog.allPackages
        let count = await Core.Protocols.SwiftPackagesCatalog.count
        #expect(entries.isEmpty, "post-#194: bundled catalog is gone; allPackages must be []")
        #expect(count < 1, "post-#194: bundled catalog is gone; count must be 0; got \(count)")

        print("   ✅ Catalog-accessor empty-contract pinned (#194)")
    }
}
