import Foundation
import Testing

@testable import NucleicCore

/// Locks the parse + component-matching of the Claude (Atlassian Statuspage) and OpenAI
/// (incident.io) Atom status feeds. The bodies below are trimmed real captures (2026-06-20);
/// re-capture and diff if the indicator ever reads wrong after a feed format change.
struct StatusFeedTests {
    // MARK: - Claude (Statuspage dialect)

    /// Three incidents: one matching no watched component (dropped), one resolved "Claude
    /// API" incident, and one active "Claude Code" incident.
    private static let claudeSample = Data("""
    <?xml version="1.0" encoding="UTF-8"?>
    <feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom">
      <id>tag:status.claude.com,2005:/history</id>
      <link rel="alternate" type="text/html" href="https://status.claude.com"/>
      <link rel="self" type="application/atom+xml" href="https://status.claude.com/history.atom"/>
      <title>Claude Status - Incident History</title>
      <updated>2026-06-20T22:08:42Z</updated>
      <author><name>Claude</name></author>
      <entry>
        <id>tag:status.claude.com,2005:Incident/1</id>
        <published>2026-06-20T18:02:23Z</published>
        <updated>2026-06-20T18:02:23Z</updated>
        <link rel="alternate" type="text/html" href="https://status.claude.com/incidents/opus"/>
        <title>Opus 4.8 errors</title>
        <content type="html">&lt;p&gt;&lt;strong&gt;Resolved&lt;/strong&gt; - This incident has been resolved.&lt;/p&gt;</content>
      </entry>
      <entry>
        <id>tag:status.claude.com,2005:Incident/2</id>
        <published>2026-06-19T08:45:47Z</published>
        <updated>2026-06-19T08:45:48Z</updated>
        <link rel="alternate" type="text/html" href="https://status.claude.com/incidents/api"/>
        <title>Elevated error rates on the Claude API</title>
        <content type="html">&lt;p&gt;&lt;strong&gt;Resolved&lt;/strong&gt; - This incident has been resolved.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Investigating&lt;/strong&gt; - We are currently investigating this issue.&lt;/p&gt;</content>
      </entry>
      <entry>
        <id>tag:status.claude.com,2005:Incident/3</id>
        <published>2026-06-18T10:00:00Z</published>
        <updated>2026-06-18T12:00:00Z</updated>
        <link rel="alternate" type="text/html" href="https://status.claude.com/incidents/code"/>
        <title>Fable not available in the model picker in Claude Code Desktop</title>
        <content type="html">&lt;p&gt;&lt;strong&gt;Monitoring&lt;/strong&gt; - A fix has been implemented and we are monitoring the results.&lt;/p&gt;</content>
      </entry>
    </feed>
    """.utf8)

    @Test func parsesClaudeFeedAndFiltersToWatchedComponents() throws {
        let feed = try StatusFeedFetcher.parse(Self.claudeSample, provider: .claude)

        // "Opus 4.8 errors" matches neither watched component, so it's dropped.
        #expect(feed.incidents.count == 2)
        // Newest first.
        #expect(feed.incidents[0].title == "Elevated error rates on the Claude API")
        #expect(feed.incidents[1].components == ["Claude Code"])

        let api = feed.incidents[0]
        #expect(api.components == ["Claude API"])
        // The latest (topmost) update is the live state, even though an earlier one isn't.
        #expect(api.state == "Resolved")
        #expect(api.isResolved)
        #expect(api.url?.absoluteString == "https://status.claude.com/incidents/api")

        let code = feed.incidents[1]
        #expect(code.state == "Monitoring")
        #expect(!code.isResolved)
    }

    @Test func claudeActiveIncidentSurfaces() throws {
        let feed = try StatusFeedFetcher.parse(Self.claudeSample, provider: .claude)
        #expect(feed.hasActiveIncident)
        #expect(feed.activeIncidents.count == 1)
        #expect(feed.activeIncidents.first?.components == ["Claude Code"])
    }

    // MARK: - OpenAI (incident.io dialect)

    /// Three incidents: a Login-only one (dropped), a resolved Codex incident (whose
    /// components list "Codex API" — so it matches both Codex and APIs), and an active API
    /// incident.
    private static let openaiSample = Data("""
    <?xml version="1.0" encoding="utf-8"?>
    <feed xmlns="http://www.w3.org/2005/Atom">
      <id>https://status.openai.com/</id>
      <title>OpenAI status</title>
      <updated>2026-06-21T01:28:47.769Z</updated>
      <generator>incident.io</generator>
      <link rel="alternate" href="https://status.openai.com/"/>
      <subtitle>OpenAI status page updates</subtitle>
      <entry>
        <title type="html"><![CDATA[chatgpt.com access issues]]></title>
        <id>https://status.openai.com//incidents/login</id>
        <link href="https://status.openai.com//incidents/login"/>
        <updated>2026-06-19T05:23:27.053Z</updated>
        <content type="html"><![CDATA[<b>Status: Resolved</b><br/><br/>All impacted services have now fully recovered.<br/><br/><b>Affected components</b><ul><li>Login (Operational)</li></ul>]]></content>
      </entry>
      <entry>
        <title type="html"><![CDATA[Codex "Selected Model is at Capacity" Error]]></title>
        <id>https://status.openai.com//incidents/codex</id>
        <link href="https://status.openai.com//incidents/codex"/>
        <updated>2026-06-18T12:00:00.000Z</updated>
        <content type="html"><![CDATA[<b>Status: Resolved</b><br/><br/>All impacted services have now fully recovered.<br/><br/><b>Affected components</b><ul><li>CLI (Operational)</li><li>Codex API (Operational)</li><li>Codex Web (Operational)</li></ul>]]></content>
      </entry>
      <entry>
        <title type="html"><![CDATA[FedRAMP workspaces and API orgs have degraded performance]]></title>
        <id>https://status.openai.com//incidents/api</id>
        <link href="https://status.openai.com//incidents/api"/>
        <updated>2026-06-18T19:41:22.518Z</updated>
        <content type="html"><![CDATA[<b>Status: Investigating</b><br/><br/>We are still investigating the issue for the listed services.<br/><br/><b>Affected components</b><ul><li>API (Degraded performance)</li></ul>]]></content>
      </entry>
    </feed>
    """.utf8)

    @Test func parsesOpenAIFeedAndFiltersToWatchedComponents() throws {
        let feed = try StatusFeedFetcher.parse(Self.openaiSample, provider: .openai)

        // The Login-only incident matches neither Codex nor APIs, so it's dropped.
        #expect(feed.incidents.count == 2)
        // Newest first.
        #expect(feed.incidents[0].id.hasSuffix("/api"))

        let fedramp = feed.incidents[0]
        #expect(fedramp.components == ["APIs"])
        #expect(fedramp.state == "Investigating")
        #expect(!fedramp.isResolved)

        // The Codex incident lists "Codex API", so it matches both watched components, in
        // provider-declared order (Codex, then APIs).
        let codex = feed.incidents[1]
        #expect(codex.components == ["Codex", "APIs"])
        #expect(codex.isResolved)
    }

    @Test func openAIFractionalTimestampParses() throws {
        let feed = try StatusFeedFetcher.parse(Self.openaiSample, provider: .openai)
        let updated = try #require(feed.incidents.first?.updatedAt)
        let components = Calendar(identifier: .gregorian)
            .dateComponents(in: TimeZone(identifier: "UTC")!, from: updated)
        #expect(components.year == 2026)
        #expect(components.month == 6)
        #expect(components.day == 18)
        #expect(components.hour == 19)
        #expect(components.minute == 41)
    }

    @Test func malformedFeedThrows() {
        #expect(throws: (any Error).self) {
            try StatusFeedFetcher.parse(Data("not a feed".utf8), provider: .claude)
        }
    }

    // MARK: - xAI (RSS feed)

    /// Three items: an API incident whose title mentions a "grok-imagine" model (must map to
    /// API only, not the Grok app, via bracket-prefix matching), an active Grok-app incident,
    /// and a Single Sign-On incident (dropped — not a watched component).
    private static let xaiSample = Data("""
    <?xml version="1.0" encoding="UTF-8" ?>
    <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
      <channel>
        <title>xAI System Status</title>
        <link>https://status.x.ai</link>
        <description>Current status and incident history of xAI's services and systems</description>
        <atom:link href="https://status.x.ai/feed.xml" rel="self" type="application/rss+xml" />
        <lastBuildDate>Wed, 17 Jun 2026 16:43:50 GMT</lastBuildDate>
        <item>
          <title>[API (us-east-1.api.x.ai)] Requests Using grok-imagine Models have Reduced Success</title>
          <link>https://status.x.ai/api-us-east-1/INC2d87987d</link>
          <guid isPermaLink="false">INC2d87987d</guid>
          <description><![CDATA[<h3>Status: RESOLVED</h3><p>Severity: available</p><h4>Updates:</h4><div><p><strong>Wed, 17 Jun 2026 12:12:07 GMT</strong></p><h3>Issue Resolved</h3><p>The incident has been resolved.</p></div>]]></description>
          <pubDate>Wed, 17 Jun 2026 12:12:07 GMT</pubDate>
          <category>available</category>
          <category>resolved</category>
        </item>
        <item>
          <title>[Grok (Web)] 4.3 losing conversation context</title>
          <link>https://status.x.ai/grok-web/INC55</link>
          <guid isPermaLink="false">INC55</guid>
          <description><![CDATA[<h3>Status: INVESTIGATING</h3><p>Severity: degraded_performance</p><h4>Updates:</h4><div><p><strong>Mon, 15 Jun 2026 09:00:00 GMT</strong></p><h3>Investigating</h3><p>We are looking into it.</p></div>]]></description>
          <pubDate>Mon, 15 Jun 2026 09:00:00 GMT</pubDate>
          <category>degraded_performance</category>
          <category>investigating</category>
        </item>
        <item>
          <title>[Single Sign-On] Login errors for some users</title>
          <link>https://status.x.ai/sso/INC9</link>
          <guid isPermaLink="false">INC9</guid>
          <description><![CDATA[<h3>Status: RESOLVED</h3><p>Severity: available</p>]]></description>
          <pubDate>Sun, 14 Jun 2026 09:00:00 GMT</pubDate>
        </item>
      </channel>
    </rss>
    """.utf8)

    @Test func parsesXaiRSSFeedAndFiltersToWatchedComponents() throws {
        let feed = try StatusFeedFetcher.parse(Self.xaiSample, provider: .xai)

        // Single Sign-On isn't watched, so it's dropped.
        #expect(feed.incidents.count == 2)
        // Newest first.
        #expect(feed.incidents[0].id == "INC2d87987d")

        let api = feed.incidents[0]
        // Bracket-prefix matching: despite "grok-imagine" in the title, this is an API
        // incident — it must NOT be miscounted under the Grok app.
        #expect(api.components == ["API"])
        #expect(api.state == "RESOLVED")
        #expect(api.isResolved)
        #expect(api.url?.absoluteString == "https://status.x.ai/api-us-east-1/INC2d87987d")

        let grok = feed.incidents[1]
        #expect(grok.components == ["Grok"])
        #expect(grok.state == "INVESTIGATING")
        #expect(!grok.isResolved)
    }

    @Test func xaiActiveIncidentSurfaces() throws {
        let feed = try StatusFeedFetcher.parse(Self.xaiSample, provider: .xai)
        #expect(feed.activeIncidents.count == 1)
        #expect(feed.activeIncidents.first?.components == ["Grok"])
    }

    @Test func xaiRFC822TimestampParses() throws {
        let feed = try StatusFeedFetcher.parse(Self.xaiSample, provider: .xai)
        let updated = try #require(feed.incidents.first?.updatedAt)
        let components = Calendar(identifier: .gregorian)
            .dateComponents(in: TimeZone(identifier: "UTC")!, from: updated)
        #expect(components.year == 2026)
        #expect(components.month == 6)
        #expect(components.day == 17)
        #expect(components.hour == 12)
        #expect(components.minute == 12)
    }

    // MARK: - Model → provider mapping (drives failover)

    @Test func mapsModelsToProviders() {
        #expect(StatusProvider.forModel("claude-opus-4-8[1m]") == .claude)
        #expect(StatusProvider.forModel("claude-sonnet-4-6") == .claude)
        #expect(StatusProvider.forModel("gpt-5.5") == .openai)
        #expect(StatusProvider.forModel("gpt-5.4-mini") == .openai)
        #expect(StatusProvider.forModel("o3-mini") == .openai)
        #expect(StatusProvider.forModel("something-codex") == .openai)
        #expect(StatusProvider.forModel("grok-build") == .xai)
        #expect(StatusProvider.forModel("grok-imagine") == .xai)
        #expect(StatusProvider.forModel("mystery-model") == nil)
        #expect(StatusProvider.forModel(nil) == nil)
    }
}
