import Foundation
import LoggingModels
@testable import Search
import SearchModels
import SharedConstants
import Testing

// MARK: - Test Doubles

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

// Unit tests for the "directory not found" fast path of each concrete
// SourceIndexingStrategy type.  These tests exercise the guard at the top of
// every indexItems(into:progress:) implementation that returns early when the
// configured directory is absent — ensuring strategies return cleanly with
// zero counts rather than throwing or crashing.
//
// SampleCodeStrategy and SwiftPackagesStrategy have no directory parameter and
// are intentionally excluded from this suite; their empty-catalog fast path is
// covered by the existing SearchTests integration suite.

@Suite("SourceIndexingStrategy missing-directory fast paths", .serialized)
struct StrategyMissingDirectoryTests {
    // MARK: - Helpers

    /// A temporary Search.Index backed by an in-memory-style path unique per test.
    private func makeIndex(in tempRoot: URL) async throws -> Search.Index {
        try await Search.Index(dbPath: tempRoot.appendingPathComponent("search.db"), logger: Logging.NoopRecording())
    }

    private func makeTempRoot() throws -> URL {
        let dir = FileManager.default.temporaryDirectory
            .appendingPathComponent("strategy-missing-\(UUID().uuidString)")
        try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
        return dir
    }

    // MARK: - AppleDocsStrategy

    @Test("AppleDocsStrategy returns zero counts when docsDirectory is absent")
    func appleDocsMissingDirectory() async throws {
        let tempRoot = try makeTempRoot()
        defer { try? FileManager.default.removeItem(at: tempRoot) }

        let missingDir = tempRoot.appendingPathComponent("docs")
        let index = try await makeIndex(in: tempRoot)
        let strategy = Search.AppleDocsStrategy(
            docsDirectory: missingDir,
            markdownStrategy: NoopMarkdownStrategy(),
            logger: Logging.NoopRecording()
        )

        let stats = try await strategy.indexItems(into: index, progress: nil)

        #expect(stats.indexed == 0)
        #expect(stats.skipped == 0)
        // #671 — missing-dir path sets wasSkipped + "no local corpus" reason
        // so the IndexBuilder renderer prints `[apple-docs] skipped (no local
        // corpus)` instead of `indexed: 0, skipped: 0` (which falsely implies
        // a failed indexing attempt).
        #expect(stats.wasSkipped == true)
        #expect(stats.skipReason == "no local corpus")
        #expect(try await index.documentCount() == 0)
    }

    @Test("AppleDocsStrategy returns zero counts when docsDirectory is empty")
    func appleDocsEmptyDirectory() async throws {
        let tempRoot = try makeTempRoot()
        defer { try? FileManager.default.removeItem(at: tempRoot) }

        let emptyDir = tempRoot.appendingPathComponent("docs")
        try FileManager.default.createDirectory(at: emptyDir, withIntermediateDirectories: true)
        let index = try await makeIndex(in: tempRoot)
        let strategy = Search.AppleDocsStrategy(
            docsDirectory: emptyDir,
            markdownStrategy: NoopMarkdownStrategy(),
            logger: Logging.NoopRecording()
        )

        let stats = try await strategy.indexItems(into: index, progress: nil)

        #expect(stats.indexed == 0)
        #expect(stats.skipped == 0)
        // #671 — empty-dir path is also a clean skip.
        #expect(stats.wasSkipped == true)
        #expect(stats.skipReason == "no documents found")
    }

    // MARK: - AppleArchiveStrategy

    @Test("AppleArchiveStrategy returns zero counts when archiveDirectory is absent")
    func appleArchiveMissingDirectory() async throws {
        let tempRoot = try makeTempRoot()
        defer { try? FileManager.default.removeItem(at: tempRoot) }

        let missingDir = tempRoot.appendingPathComponent("archive")
        let index = try await makeIndex(in: tempRoot)
        let strategy = Search.AppleArchiveStrategy(archiveDirectory: missingDir, logger: Logging.NoopRecording())

        let stats = try await strategy.indexItems(into: index, progress: nil)

        #expect(stats.indexed == 0)
        #expect(stats.skipped == 0)
        // #671 — missing-dir is a clean skip with the "no local corpus" reason.
        #expect(stats.wasSkipped == true)
        #expect(stats.skipReason == "no local corpus")
    }

    // MARK: - HIGStrategy

    @Test("HIGStrategy returns zero counts when higDirectory is absent")
    func higMissingDirectory() async throws {
        let tempRoot = try makeTempRoot()
        defer { try? FileManager.default.removeItem(at: tempRoot) }

        let missingDir = tempRoot.appendingPathComponent("hig")
        let index = try await makeIndex(in: tempRoot)
        let strategy = Search.HIGStrategy(higDirectory: missingDir, logger: Logging.NoopRecording())

        let stats = try await strategy.indexItems(into: index, progress: nil)

        #expect(stats.indexed == 0)
        #expect(stats.skipped == 0)
        // #671 — missing-dir clean-skip + reason
        #expect(stats.wasSkipped == true)
        #expect(stats.skipReason == "no local corpus")
    }

    // MARK: - SwiftEvolutionStrategy

    @Test("SwiftEvolutionStrategy returns zero counts when evolutionDirectory is absent")
    func swiftEvolutionMissingDirectory() async throws {
        let tempRoot = try makeTempRoot()
        defer { try? FileManager.default.removeItem(at: tempRoot) }

        let missingDir = tempRoot.appendingPathComponent("swift-evolution")
        let index = try await makeIndex(in: tempRoot)
        let strategy = Search.SwiftEvolutionStrategy(evolutionDirectory: missingDir, logger: Logging.NoopRecording())

        let stats = try await strategy.indexItems(into: index, progress: nil)

        #expect(stats.indexed == 0)
        #expect(stats.skipped == 0)
        // #671 — missing-dir clean-skip + reason
        #expect(stats.wasSkipped == true)
        #expect(stats.skipReason == "no local corpus")
    }

    @Test("SwiftEvolutionStrategy skips non-accepted proposals")
    func swiftEvolutionSkipsWithdrawnProposals() async throws {
        let tempRoot = try makeTempRoot()
        defer { try? FileManager.default.removeItem(at: tempRoot) }

        let evolutionDir = tempRoot.appendingPathComponent("swift-evolution")
        try FileManager.default.createDirectory(at: evolutionDir, withIntermediateDirectories: true)
        // A withdrawn proposal — should not be indexed.
        let withdrawnContent = """
        # SE-9999 Withdrawn Feature
        * Status: **Withdrawn**
        This proposal has been withdrawn.
        """
        try withdrawnContent.write(
            to: evolutionDir.appendingPathComponent("SE-9999-withdrawn.md"),
            atomically: true, encoding: .utf8
        )

        let index = try await makeIndex(in: tempRoot)
        let strategy = Search.SwiftEvolutionStrategy(evolutionDirectory: evolutionDir, logger: Logging.NoopRecording())
        let stats = try await strategy.indexItems(into: index, progress: nil)

        #expect(stats.skipped == 1)
        #expect(stats.indexed == 0)
        #expect(try await index.documentCount() == 0)
    }

    // MARK: - SwiftOrgStrategy

    @Test("SwiftOrgStrategy returns zero counts when swiftOrgDirectory is absent")
    func swiftOrgMissingDirectory() async throws {
        let tempRoot = try makeTempRoot()
        defer { try? FileManager.default.removeItem(at: tempRoot) }

        let missingDir = tempRoot.appendingPathComponent("swift-org")
        let index = try await makeIndex(in: tempRoot)
        let strategy = Search.SwiftOrgStrategy(
            swiftOrgDirectory: missingDir,
            markdownStrategy: NoopMarkdownStrategy(),
            logger: Logging.NoopRecording()
        )

        let stats = try await strategy.indexItems(into: index, progress: nil)

        #expect(stats.indexed == 0)
        #expect(stats.skipped == 0)
        // #671 — missing-dir clean-skip + reason
        #expect(stats.wasSkipped == true)
        #expect(stats.skipReason == "no local corpus")
    }

    // MARK: - #671 — Default initializer preserves prior wasSkipped/skipReason behaviour

    @Test("#671 — IndexStats default init produces wasSkipped == false, skipReason == nil")
    func indexStatsDefaultInitNoSkip() {
        // Existing strategies that actually run indexing keep their pre-#671
        // call shape unchanged. Asserting on the default values pins the
        // backward-compatible behaviour: an indexed-N stats value is never
        // misread as a clean-skip line.
        let stats = Search.IndexStats(source: "apple-docs", indexed: 100, skipped: 5)
        #expect(stats.wasSkipped == false)
        #expect(stats.skipReason == nil)
    }
}
