@testable import Core
@testable import CoreJSONParser
import CoreProtocols
@testable import Crawler
import CrawlerModels
import Foundation
import LoggingModels
import SharedConstants
import Testing

// MARK: - Crawler Tests

// Tests for the Crawler.AppleDocs web crawling engine
// Note: These are integration tests that use real WKWebView

@Suite("Crawler")
struct CrawlerTests {
    // MARK: - Initialization Tests

    @Test("Crawler initializes with configuration")
    @MainActor
    func crawlerInitialization() async throws {
        let tempDir = FileManager.default.temporaryDirectory
            .appendingPathComponent(UUID().uuidString)

        let config = try Shared.Configuration(
            crawler: Shared.Configuration.Crawler(
                startURL: #require(URL(string: "https://developer.apple.com")),
                maxPages: 5,
                outputDirectory: tempDir
            ),
            changeDetection: Shared.Configuration.ChangeDetection(
                enabled: false,
                metadataFile: tempDir.appendingPathComponent("metadata.json"),
                outputDirectory: tempDir
            ),
            output: Shared.Configuration.Output()
        )

        let crawler = await Crawler.AppleDocs(
            configuration: config,
            htmlParser: Crawler.NoopHTMLParserStrategy(),
            appleJSONParser: Crawler.NoopAppleJSONParserStrategy(),
            priorityPackageStrategy: Crawler.NoopPriorityPackageStrategy(),
            logger: Logging.NoopRecording()
        )

        // If we get here without crashing, initialization worked
        _ = crawler

        // Cleanup
        try? FileManager.default.removeItem(at: tempDir)
    }

    // MARK: - URL Normalization Tests

    @Test("URLUtilities normalize preserves path but removes trailing slash")
    func urlNormalizePreservesPath() throws {
        let url = try #require(URL(string: "https://example.com/path/"))
        let normalized = Shared.Models.URLUtilities.normalize(url)

        // Normalization removes fragments, query params, and trailing slashes
        #expect(normalized?.path == "/path")
    }

    @Test("URLUtilities normalize removes fragments")
    func urlNormalizeRemovesFragments() throws {
        let url = try #require(URL(string: "https://example.com/path#section"))
        let normalized = Shared.Models.URLUtilities.normalize(url)

        #expect(normalized?.fragment == nil)
        #expect(try !#require(normalized?.absoluteString.contains("#")))
    }

    @Test("URLUtilities normalize removes query parameters")
    func urlNormalizeRemovesQueryParams() throws {
        let url = try #require(URL(string: "https://example.com/path?param=value"))
        let normalized = Shared.Models.URLUtilities.normalize(url)

        #expect(normalized?.query == nil)
        #expect(try !#require(normalized?.absoluteString.contains("?")))
    }

    @Test("URLUtilities normalize lowercases Apple documentation paths")
    func urlNormalizeLowercasesAppleDocumentationPaths() throws {
        let uppercase = try #require(URL(string: "https://developer.apple.com/documentation/Cinematic/CNAssetInfo-2ata2"))
        let lowercase = try #require(URL(string: "https://developer.apple.com/documentation/cinematic/cnassetinfo-2ata2"))

        #expect(Shared.Models.URLUtilities.normalize(uppercase) == Shared.Models.URLUtilities.normalize(lowercase))
        #expect(Shared.Models.URLUtilities.normalize(uppercase)?.path == "/documentation/cinematic/cnassetinfo-2ata2")
    }

    @Test("URLUtilities normalize preserves method disambiguator dashes")
    func urlNormalizePreservesMethodDisambiguatorDashes() throws {
        let url = try #require(URL(string: "https://developer.apple.com/documentation/Cinematic/CNAssetInfo-2ata2"))

        #expect(Shared.Models.URLUtilities.normalize(url)?.lastPathComponent == "cnassetinfo-2ata2")
    }

    @Test("URLUtilities normalize keeps underscores intact (installer_js safety)")
    func urlNormalizePreservesUnderscoresInPath() throws {
        // Apple does not redirect /documentation/installer-js to
        // /documentation/installer_js — the dash form returns 404. A naive
        // underscore→dash collapse in URLUtilities would silently break the
        // entire installer_js framework on every crawl. Verify we never make
        // that change.
        let url = try #require(URL(string: "https://developer.apple.com/documentation/installer_js/license"))
        let normalized = Shared.Models.URLUtilities.normalize(url)

        #expect(normalized?.path == "/documentation/installer_js/license")
        #expect(try !#require(normalized?.absoluteString.contains("installer-js")))
    }

    @Test("URLUtilities normalize converts underscore sub-page slug to dash")
    func urlNormalizeConvertsUnderscoreSubpageToDash() throws {
        let url = try #require(URL(string: "https://developer.apple.com/documentation/corelocation/getting_heading_and_course_information"))
        let normalized = Shared.Models.URLUtilities.normalize(url)

        #expect(normalized?.path == "/documentation/corelocation/getting-heading-and-course-information")
    }

    @Test("URLUtilities normalize preserves framework slug at depth 2 and normalizes sub-page underscore")
    func urlNormalizePreservesFrameworkSlugNormalizesSubpage() throws {
        // driverkit/driverkit_constants is one of the 31 duplicate-cluster pairs from #285.
        // The framework slug "driverkit" at depth 2 must be left untouched;
        // only "driverkit_constants" at depth 3 is collapsed to dashes.
        let url = try #require(URL(string: "https://developer.apple.com/documentation/driverkit/driverkit_constants"))
        let normalized = Shared.Models.URLUtilities.normalize(url)

        #expect(normalized?.path == "/documentation/driverkit/driverkit-constants")
    }

    @Test("URLUtilities normalize converts underscores at multiple sub-page depths")
    func urlNormalizeConvertsUnderscoresAtMultipleDepths() throws {
        let url = try #require(URL(string: "https://developer.apple.com/documentation/swiftui/some_class/some_method"))
        let normalized = Shared.Models.URLUtilities.normalize(url)

        #expect(normalized?.path == "/documentation/swiftui/some-class/some-method")
    }

    @Test("URLUtilities normalize does not touch non-documentation URL underscores")
    func urlNormalizeDoesNotTouchNonDocsPaths() throws {
        let url = try #require(URL(string: "https://developer.apple.com/videos/play/wwdc2023/10_video"))
        let normalized = Shared.Models.URLUtilities.normalize(url)

        #expect(normalized?.path == "/videos/play/wwdc2023/10_video")
    }

    @Test("URLUtilities normalize strips fragment and query and collapses sub-page underscore")
    func urlNormalizeStripsFragmentQueryAndNormalizesUnderscore() throws {
        let url = try #require(URL(string: "https://developer.apple.com/documentation/swiftui/some_method?foo=1#bar"))
        let normalized = Shared.Models.URLUtilities.normalize(url)

        #expect(normalized?.path == "/documentation/swiftui/some-method")
        #expect(normalized?.query == nil)
        #expect(normalized?.fragment == nil)
    }

    @Test("URLUtilities normalize lowercases before collapsing underscore to dash")
    func urlNormalizeLowercasesBeforeCollapsingUnderscore() throws {
        let url = try #require(URL(string: "https://developer.apple.com/documentation/SwiftUI/Some_Method"))
        let normalized = Shared.Models.URLUtilities.normalize(url)

        #expect(normalized?.path == "/documentation/swiftui/some-method")
    }

    @Test("URLUtilities normalize does not crash on /documentation with no framework slug")
    func urlNormalizeHandlesDocumentationOnlyPath() throws {
        // Regression: normalizeDocPath used to trap with "Range requires lowerBound <= upperBound"
        // when documentation was found but had fewer than 2 following path segments.
        let url = try #require(URL(string: "https://developer.apple.com/documentation"))
        let normalized = Shared.Models.URLUtilities.normalize(url)

        #expect(normalized?.path == "/documentation")
    }

    // MARK: - Framework Extraction Tests

    @Test("URLUtilities extracts framework from Apple docs URL")
    func extractFrameworkFromAppleDocs() throws {
        let url = try #require(URL(string: "https://developer.apple.com/documentation/swift/array"))
        let framework = Shared.Models.URLUtilities.extractFramework(from: url)

        #expect(framework == "swift")
    }

    @Test("URLUtilities extracts framework from nested path")
    func extractFrameworkFromNestedPath() throws {
        let url = try #require(URL(string: "https://developer.apple.com/documentation/uikit/uiview/animator"))
        let framework = Shared.Models.URLUtilities.extractFramework(from: url)

        #expect(framework == "uikit")
    }

    @Test("URLUtilities returns root for non-documentation URLs")
    func extractFrameworkReturnsRootForNonDocs() throws {
        let url = try #require(URL(string: "https://example.com/some/path"))
        let framework = Shared.Models.URLUtilities.extractFramework(from: url)

        #expect(framework == "root")
    }

    // MARK: - Filename Generation Tests

    @Test("URLUtilities generates filename from URL")
    func generateFilenameFromURL() throws {
        let url = try #require(URL(string: "https://developer.apple.com/documentation/swift/array"))
        let filename = Shared.Models.URLUtilities.filename(from: url)

        #expect(filename.contains("array"))
        #expect(!filename.contains("/")) // No slashes in filename
    }

    @Test("URLUtilities handles complex paths in filename")
    func generateFilenameFromComplexPath() throws {
        let url = try #require(URL(string: "https://developer.apple.com/documentation/uikit/uiview/1622417-addsubview"))
        let filename = Shared.Models.URLUtilities.filename(from: url)

        #expect(!filename.isEmpty)
        #expect(!filename.contains("/"))
    }

    // MARK: - Hash Utilities Tests

    @Test("HashUtilities SHA256 generates consistent hashes")
    func hashUtilitiesConsistentHash() {
        let content = "Test content for hashing"

        let hash1 = Shared.Models.HashUtilities.sha256(of: content)
        let hash2 = Shared.Models.HashUtilities.sha256(of: content)

        #expect(hash1 == hash2)
        #expect(hash1.count == 64) // SHA256 produces 64 hex characters
    }

    @Test("HashUtilities SHA256 produces different hashes for different content")
    func hashUtilitiesDifferentContentDifferentHash() {
        let content1 = "Content A"
        let content2 = "Content B"

        let hash1 = Shared.Models.HashUtilities.sha256(of: content1)
        let hash2 = Shared.Models.HashUtilities.sha256(of: content2)

        #expect(hash1 != hash2)
    }

    @Test("HashUtilities SHA256 handles empty string")
    func hashUtilitiesEmptyString() {
        let hash = Shared.Models.HashUtilities.sha256(of: "")

        #expect(!hash.isEmpty)
        #expect(hash.count == 64)
    }

    // MARK: - CrawlStatistics Tests

    @Test("CrawlStatistics initializes with zeros")
    func statisticsInitializesWithZeros() {
        let stats = Shared.Models.CrawlStatistics()

        #expect(stats.totalPages == 0)
        #expect(stats.newPages == 0)
        #expect(stats.updatedPages == 0)
        #expect(stats.skippedPages == 0)
        #expect(stats.errors == 0)
    }

    @Test("CrawlStatistics is Codable")
    func statisticsIsCodable() throws {
        var stats = Shared.Models.CrawlStatistics()
        stats.totalPages = 100
        stats.newPages = 50
        stats.updatedPages = 30
        stats.skippedPages = 20
        stats.errors = 5
        stats.startTime = Date()
        stats.endTime = Date().addingTimeInterval(3600)

        let encoder = JSONEncoder()
        let data = try encoder.encode(stats)

        let decoder = JSONDecoder()
        let decoded = try decoder.decode(Shared.Models.CrawlStatistics.self, from: data)

        #expect(decoded.totalPages == 100)
        #expect(decoded.newPages == 50)
        #expect(decoded.updatedPages == 30)
        #expect(decoded.skippedPages == 20)
        #expect(decoded.errors == 5)
    }

    // MARK: - Crawler.AppleDocsProgress Tests

    @Test("Crawler.AppleDocsProgress calculates percentage")
    func progressCalculatesPercentage() throws {
        let stats = Shared.Models.CrawlStatistics()
        let progress = try Crawler.AppleDocsProgress(
            currentURL: #require(URL(string: "https://example.com")),
            visitedCount: 10,
            totalPages: 100,
            stats: stats
        )

        #expect(progress.visitedCount == 10)
        #expect(progress.totalPages == 100)
        #expect(progress.percentage == 10.0)
    }

    // MARK: - Integration Tests (Tagged as .integration)

    @Test("Crawler respects max pages limit", .tags(.integration))
    @MainActor
    func crawlerRespectsMaxPages() async throws {
        let tempDir = FileManager.default.temporaryDirectory
            .appendingPathComponent(UUID().uuidString)

        let config = try Shared.Configuration(
            crawler: Shared.Configuration.Crawler(
                startURL: #require(URL(string: "https://example.com")),
                maxPages: 1, // Limit to 1 page
                outputDirectory: tempDir,
                requestDelay: 0.1
            ),
            changeDetection: Shared.Configuration.ChangeDetection(
                enabled: false,
                metadataFile: tempDir.appendingPathComponent("metadata.json"),
                outputDirectory: tempDir
            ),
            output: Shared.Configuration.Output()
        )

        let crawler = await Crawler.AppleDocs(
            configuration: config,
            htmlParser: Crawler.NoopHTMLParserStrategy(),
            appleJSONParser: Crawler.NoopAppleJSONParserStrategy(),
            priorityPackageStrategy: Crawler.NoopPriorityPackageStrategy(),
            logger: Logging.NoopRecording()
        )

        // Note: This may fail to load the actual page (network required),
        // but it tests that the crawler can be instantiated and attempt to crawl
        do {
            let stats = try await crawler.crawl()
            #expect(stats.totalPages <= 1)
        } catch {
            // Network errors are acceptable in unit tests
            // The important thing is the crawler initialized and attempted to run
        }

        // Cleanup
        try? FileManager.default.removeItem(at: tempDir)
    }

    @Test("Crawler creates output directory", .tags(.integration))
    @MainActor
    func crawlerCreatesOutputDirectory() async throws {
        let tempDir = FileManager.default.temporaryDirectory
            .appendingPathComponent(UUID().uuidString)

        // Ensure directory doesn't exist yet
        try? FileManager.default.removeItem(at: tempDir)
        #expect(!FileManager.default.fileExists(atPath: tempDir.path))

        let config = try Shared.Configuration(
            crawler: Shared.Configuration.Crawler(
                startURL: #require(URL(string: "https://example.com")),
                maxPages: 1,
                outputDirectory: tempDir,
                requestDelay: 0.1
            ),
            changeDetection: Shared.Configuration.ChangeDetection(
                enabled: false,
                metadataFile: tempDir.appendingPathComponent("metadata.json"),
                outputDirectory: tempDir
            ),
            output: Shared.Configuration.Output()
        )

        let crawler = await Crawler.AppleDocs(
            configuration: config,
            htmlParser: Crawler.NoopHTMLParserStrategy(),
            appleJSONParser: Crawler.NoopAppleJSONParserStrategy(),
            priorityPackageStrategy: Crawler.NoopPriorityPackageStrategy(),
            logger: Logging.NoopRecording()
        )

        do {
            _ = try await crawler.crawl()
        } catch {
            // Network errors expected - we're testing directory creation
        }

        // Directory should exist even if crawl failed
        #expect(FileManager.default.fileExists(atPath: tempDir.path))

        // Cleanup
        try? FileManager.default.removeItem(at: tempDir)
    }

    // MARK: - Known Problematic Pages Tests (Issue #25)

    @Test("LAPACK functions page URL is recognized as problematic", .tags(.integration))
    func lapackFunctionsURLIsProblematic() throws {
        // This page has 1600+ LAPACK/BLAS routines and crashes WKWebView due to OOM
        // URL: https://developer.apple.com/documentation/accelerate/lapack-functions
        // The JSON API alternative: https://developer.apple.com/tutorials/data/documentation/accelerate/lapack-functions.json
        // returns 7.2MB of structured data vs trying to render a massive DOM

        let lapackURL = try #require(URL(string: "https://developer.apple.com/documentation/accelerate/lapack-functions"))

        // The URL should normalize correctly
        let normalized = Shared.Models.URLUtilities.normalize(lapackURL)
        #expect(normalized != nil)
        #expect(normalized?.absoluteString == "https://developer.apple.com/documentation/accelerate/lapack-functions")

        // Framework extraction should work
        let framework = Shared.Models.URLUtilities.extractFramework(from: lapackURL)
        #expect(framework == "accelerate")

        // Filename generation should work
        let filename = Shared.Models.URLUtilities.filename(from: lapackURL)
        #expect(filename.contains("lapack-functions") || filename.contains("lapack_functions"))
    }

    @Test("Apple JSON API endpoint can be derived from documentation URL")
    func appleJSONAPIEndpoint() throws {
        // Apple's documentation uses a JSON API under the hood
        // Web: https://developer.apple.com/documentation/accelerate/lapack-functions
        // JSON: https://developer.apple.com/tutorials/data/documentation/accelerate/lapack-functions.json

        let docURL = try #require(URL(string: "https://developer.apple.com/documentation/accelerate/lapack-functions"))

        // Derive JSON API URL from documentation URL
        let path = docURL.path // "/documentation/accelerate/lapack-functions"
        let jsonURLString = "https://developer.apple.com/tutorials/data\(path).json"
        let jsonURL = URL(string: jsonURLString)

        #expect(jsonURL != nil)
        #expect(jsonURL?.absoluteString == "https://developer.apple.com/tutorials/data/documentation/accelerate/lapack-functions.json")
    }

    // MARK: - Redirect Storage Path Tests (Issue #277)

    @Test("documentationURL(from:) reverses a JSON API URL to a documentation URL")
    func documentationURLReversal() throws {
        let jsonAPIURL = try #require(
            URL(string: "https://developer.apple.com/tutorials/data/documentation/professional-video-applications/overview.json")
        )
        let docURL = Core.JSONParser.AppleJSONToMarkdown.documentationURL(from: jsonAPIURL)

        #expect(docURL?.absoluteString == "https://developer.apple.com/documentation/professional-video-applications/overview")
    }

    @Test("documentationURL(from:) round-trips with jsonAPIURL(from:)")
    func documentationURLRoundTrip() throws {
        let original = try #require(
            URL(string: "https://developer.apple.com/documentation/professional-video-applications/overview")
        )
        let jsonURL = try #require(Core.JSONParser.AppleJSONToMarkdown.jsonAPIURL(from: original))
        let reversed = try #require(Core.JSONParser.AppleJSONToMarkdown.documentationURL(from: jsonURL))

        #expect(reversed.absoluteString == original.absoluteString)
    }

    @Test("documentationURL(from:) returns nil for non-JSON-API URLs")
    func documentationURLReturnsNilForNonAPIURL() throws {
        // A plain documentation URL is not a JSON API URL — should return nil
        let docURL = try #require(
            URL(string: "https://developer.apple.com/documentation/swift/array")
        )
        #expect(Core.JSONParser.AppleJSONToMarkdown.documentationURL(from: docURL) == nil)

        // A non-Apple URL should return nil
        let externalURL = try #require(URL(string: "https://example.com/tutorials/data/documentation/foo.json"))
        #expect(Core.JSONParser.AppleJSONToMarkdown.documentationURL(from: externalURL) == nil)
    }

    @Test("documentationURL(from:) handles the professional_video_applications slug migration")
    func documentationURLHandlesUnderscoreToHyphenMigration() throws {
        // Regression for Issue #277: Apple migrated professional_video_applications → professional-video-applications
        // The JSON API for the old URL 301s to the new one; response.url has the dash form.
        let redirectedJSONURL = try #require(
            URL(string: "https://developer.apple.com/tutorials/data/documentation/professional-video-applications.json")
        )
        let canonical = try #require(Core.JSONParser.AppleJSONToMarkdown.documentationURL(from: redirectedJSONURL))

        #expect(canonical.absoluteString == "https://developer.apple.com/documentation/professional-video-applications")
        // Confirm the canonical URL uses dashes (not underscores), so the corpus stores
        // content under the new slug rather than the stale request URL.
        #expect(!canonical.absoluteString.contains("professional_video_applications"))
    }

    @Test("JSONContentFetcher FetchResult carries the post-redirect URL")
    func jsonContentFetcherReturnsPostRedirectURL() async throws {
        // Register a mock URLProtocol that issues a 301 redirect then serves content.
        // This verifies that JSONContentFetcher.fetch captures response.url, not the request URL.
        let requestURL = try #require(URL(string: "https://developer.apple.com/tutorials/data/documentation/professional_video_applications.json"))
        let finalURL = try #require(URL(string: "https://developer.apple.com/tutorials/data/documentation/professional-video-applications.json"))

        let config = URLSessionConfiguration.ephemeral
        config.protocolClasses = [RedirectMockURLProtocol.self]
        config.timeoutIntervalForRequest = 5

        // Use a direct URLSession call mimicking JSONContentFetcher behaviour
        let session = URLSession(configuration: config)
        let (_, response) = try await session.data(from: requestURL)
        let capturedURL = response.url ?? requestURL

        // The session follows the redirect; response.url must reflect the final URL.
        #expect(capturedURL.absoluteString == finalURL.absoluteString)
    }

    // MARK: - htmlLinkAugmentation Configuration Tests (#203)

    @Test("CrawlerConfiguration htmlLinkAugmentation defaults to true")
    func htmlLinkAugmentationDefaultsToTrue() throws {
        let config = try Shared.Configuration.Crawler(
            startURL: #require(URL(string: "https://developer.apple.com/documentation")),
            outputDirectory: FileManager.default.temporaryDirectory
        )
        #expect(config.htmlLinkAugmentation == true)
    }

    @Test("CrawlerConfiguration htmlLinkAugmentationMaxRefs defaults to 10")
    func htmlLinkAugmentationMaxRefsDefaultsToTen() throws {
        let config = try Shared.Configuration.Crawler(
            startURL: #require(URL(string: "https://developer.apple.com/documentation")),
            outputDirectory: FileManager.default.temporaryDirectory
        )
        #expect(config.htmlLinkAugmentationMaxRefs == 10)
    }

    @Test("CrawlerConfiguration htmlLinkAugmentation can be disabled explicitly")
    func htmlLinkAugmentationCanBeDisabled() throws {
        let config = try Shared.Configuration.Crawler(
            startURL: #require(URL(string: "https://developer.apple.com/documentation")),
            outputDirectory: FileManager.default.temporaryDirectory,
            htmlLinkAugmentation: false
        )
        #expect(config.htmlLinkAugmentation == false)
    }

    @Test("CrawlerConfiguration htmlLinkAugmentationMaxRefs can be raised to disable the heuristic")
    func htmlLinkAugmentationMaxRefsCanBeRaised() throws {
        let config = try Shared.Configuration.Crawler(
            startURL: #require(URL(string: "https://developer.apple.com/documentation")),
            outputDirectory: FileManager.default.temporaryDirectory,
            htmlLinkAugmentationMaxRefs: .max
        )
        #expect(config.htmlLinkAugmentationMaxRefs == .max)
    }

    @Test("CrawlerConfiguration decodes legacy JSON without augmentation fields with defaults")
    func htmlLinkAugmentationDecodesLegacyJSON() throws {
        let tempDir = FileManager.default.temporaryDirectory
        let json = """
        {
            "startURL": "https://developer.apple.com/documentation",
            "allowedPrefixes": ["https://developer.apple.com/documentation"],
            "maxPages": 100,
            "maxDepth": 15,
            "outputDirectory": "\(tempDir.path)",
            "requestDelay": 0.05,
            "retryAttempts": 3
        }
        """
        let data = try #require(Data(json.utf8))
        let config = try JSONDecoder().decode(Shared.Configuration.Crawler.self, from: data)
        #expect(config.htmlLinkAugmentation == true)
        #expect(config.htmlLinkAugmentationMaxRefs == 10)
    }

    @Test("CrawlerConfiguration decodes explicit augmentation fields from JSON")
    func htmlLinkAugmentationDecodesExplicitValues() throws {
        let tempDir = FileManager.default.temporaryDirectory
        let json = """
        {
            "startURL": "https://developer.apple.com/documentation",
            "allowedPrefixes": ["https://developer.apple.com/documentation"],
            "maxPages": 100,
            "maxDepth": 15,
            "outputDirectory": "\(tempDir.path)",
            "requestDelay": 0.05,
            "retryAttempts": 3,
            "htmlLinkAugmentation": false,
            "htmlLinkAugmentationMaxRefs": 25
        }
        """
        let data = try #require(Data(json.utf8))
        let config = try JSONDecoder().decode(Shared.Configuration.Crawler.self, from: data)
        #expect(config.htmlLinkAugmentation == false)
        #expect(config.htmlLinkAugmentationMaxRefs == 25)
    }

    @Test("CrawlerConfiguration encode + decode round-trips the augmentation fields")
    func htmlLinkAugmentationRoundTripsThroughEncode() throws {
        let original = try Shared.Configuration.Crawler(
            startURL: #require(URL(string: "https://developer.apple.com/documentation")),
            outputDirectory: FileManager.default.temporaryDirectory,
            htmlLinkAugmentation: false,
            htmlLinkAugmentationMaxRefs: 5
        )
        let data = try JSONEncoder().encode(original)
        let decoded = try JSONDecoder().decode(Shared.Configuration.Crawler.self, from: data)
        #expect(decoded.htmlLinkAugmentation == false)
        #expect(decoded.htmlLinkAugmentationMaxRefs == 5)
    }
}

// MARK: - Mock URLProtocol for redirect test

/// Simulates a 301 redirect from professional_video_applications → professional-video-applications
final class RedirectMockURLProtocol: URLProtocol {
    private static let requestURLString = "https://developer.apple.com/tutorials/data/documentation/professional_video_applications.json"
    private static let finalURLString = "https://developer.apple.com/tutorials/data/documentation/professional-video-applications.json"

    override class func canInit(with request: URLRequest) -> Bool {
        guard let url = request.url?.absoluteString else { return false }
        return url == requestURLString || url == finalURLString
    }

    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        request
    }

    override func startLoading() {
        guard let url = request.url else {
            client?.urlProtocol(self, didFailWithError: URLError(.badURL))
            return
        }

        if url.absoluteString == Self.requestURLString,
           let redirectURL = URL(string: Self.finalURLString) {
            // Issue a 301 redirect
            let redirectResponse = HTTPURLResponse(
                url: url,
                statusCode: 301,
                httpVersion: "HTTP/1.1",
                headerFields: ["Location": Self.finalURLString]
            )!
            let redirectRequest = URLRequest(url: redirectURL)
            client?.urlProtocol(self, wasRedirectedTo: redirectRequest, redirectResponse: redirectResponse)
            return
        }

        // Serve a minimal valid JSON response for the final URL
        let json = Data("""
        {"metadata":{"title":"Professional Video Applications"}}
        """.utf8)
        let response = HTTPURLResponse(
            url: url,
            statusCode: 200,
            httpVersion: "HTTP/1.1",
            headerFields: ["Content-Type": "application/json"]
        )!
        client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
        client?.urlProtocol(self, didLoad: json)
        client?.urlProtocolDidFinishLoading(self)
    }

    override func stopLoading() {}
}
