import Foundation
import Testing
@testable import OpenClaw

struct CronModelsTests {
    private func makeCronJob(
        name: String,
        payloadText: String,
        state: CronJobState = CronJobState()) -> CronJob
    {
        CronJob(
            id: "x",
            agentId: nil,
            name: name,
            description: nil,
            enabled: true,
            deleteAfterRun: nil,
            createdAtMs: 0,
            updatedAtMs: 0,
            schedule: .at(at: "2026-02-03T18:00:00Z"),
            sessionTarget: .main,
            wakeMode: .now,
            payload: .systemEvent(text: payloadText),
            delivery: nil,
            state: state)
    }

    @Test func `schedule at encodes and decodes`() throws {
        let schedule = CronSchedule.at(at: "2026-02-03T18:00:00Z")
        let data = try JSONEncoder().encode(schedule)
        let decoded = try JSONDecoder().decode(CronSchedule.self, from: data)
        #expect(decoded == schedule)
    }

    @Test func `schedule at decodes legacy at ms`() throws {
        let json = """
        {"kind":"at","atMs":1700000000000}
        """
        let decoded = try JSONDecoder().decode(CronSchedule.self, from: Data(json.utf8))
        if case let .at(at) = decoded {
            #expect(at.hasPrefix("2023-"))
        } else {
            #expect(Bool(false))
        }
    }

    @Test func `schedule every encodes and decodes with anchor`() throws {
        let schedule = CronSchedule.every(everyMs: 5000, anchorMs: 10000)
        let data = try JSONEncoder().encode(schedule)
        let decoded = try JSONDecoder().decode(CronSchedule.self, from: data)
        #expect(decoded == schedule)
    }

    @Test func `schedule cron encodes and decodes with timezone`() throws {
        let schedule = CronSchedule.cron(expr: "*/5 * * * *", tz: "Europe/Vienna")
        let data = try JSONEncoder().encode(schedule)
        let decoded = try JSONDecoder().decode(CronSchedule.self, from: data)
        #expect(decoded == schedule)
    }

    @Test func `payload agent turn encodes and decodes`() throws {
        let payload = CronPayload.agentTurn(
            message: "hello",
            thinking: "low",
            timeoutSeconds: 15,
            deliver: true,
            channel: "whatsapp",
            to: "+15551234567",
            bestEffortDeliver: false)
        let data = try JSONEncoder().encode(payload)
        let decoded = try JSONDecoder().decode(CronPayload.self, from: data)
        #expect(decoded == payload)
    }

    @Test func `job encodes and decodes delete after run`() throws {
        let job = CronJob(
            id: "job-1",
            agentId: nil,
            name: "One-shot",
            description: nil,
            enabled: true,
            deleteAfterRun: true,
            createdAtMs: 0,
            updatedAtMs: 0,
            schedule: .at(at: "2026-02-03T18:00:00Z"),
            sessionTarget: .main,
            wakeMode: .now,
            payload: .systemEvent(text: "ping"),
            delivery: nil,
            state: CronJobState())
        let data = try JSONEncoder().encode(job)
        let decoded = try JSONDecoder().decode(CronJob.self, from: data)
        #expect(decoded.deleteAfterRun == true)
    }

    @Test func `schedule decode rejects unknown kind`() {
        let json = """
        {"kind":"wat","at":"2026-02-03T18:00:00Z"}
        """
        #expect(throws: DecodingError.self) {
            _ = try JSONDecoder().decode(CronSchedule.self, from: Data(json.utf8))
        }
    }

    @Test func `payload decode rejects unknown kind`() {
        let json = """
        {"kind":"wat","text":"hello"}
        """
        #expect(throws: DecodingError.self) {
            _ = try JSONDecoder().decode(CronPayload.self, from: Data(json.utf8))
        }
    }

    @Test func `display name trims whitespace and falls back`() {
        let base = self.makeCronJob(name: "  hello  ", payloadText: "hi")
        #expect(base.displayName == "hello")

        var unnamed = base
        unnamed.name = "   "
        #expect(unnamed.displayName == "Untitled job")
    }

    @Test func `next run date and last run date derive from state`() {
        let job = self.makeCronJob(
            name: "t",
            payloadText: "hi",
            state: CronJobState(
                nextRunAtMs: 1_700_000_000_000,
                runningAtMs: nil,
                lastRunAtMs: 1_700_000_050_000,
                lastStatus: nil,
                lastError: nil,
                lastDurationMs: nil))
        #expect(job.nextRunDate == Date(timeIntervalSince1970: 1_700_000_000))
        #expect(job.lastRunDate == Date(timeIntervalSince1970: 1_700_000_050))
    }

    @Test func `decode cron list response skips malformed jobs`() throws {
        let json = """
        {
          "jobs": [
            {
              "id": "good",
              "name": "Healthy job",
              "enabled": true,
              "createdAtMs": 1,
              "updatedAtMs": 2,
              "schedule": { "kind": "at", "at": "2026-03-01T10:00:00Z" },
              "sessionTarget": "main",
              "wakeMode": "now",
              "payload": { "kind": "systemEvent", "text": "hello" },
              "state": {}
            },
            {
              "id": "bad",
              "name": "Broken job",
              "enabled": true,
              "createdAtMs": 1,
              "updatedAtMs": 2,
              "schedule": { "kind": "at", "at": "2026-03-01T10:00:00Z" },
              "payload": { "kind": "systemEvent", "text": "hello" },
              "state": {}
            }
          ],
          "total": 2,
          "offset": 0,
          "limit": 50,
          "hasMore": false,
          "nextOffset": null
        }
        """

        let jobs = try GatewayConnection.decodeCronListResponse(Data(json.utf8))

        #expect(jobs.count == 1)
        #expect(jobs.first?.id == "good")
    }

    @Test func `decode cron runs response skips malformed entries`() throws {
        let json = """
        {
          "entries": [
            {
              "ts": 1,
              "jobId": "good",
              "action": "finished",
              "status": "ok"
            },
            {
              "jobId": "bad",
              "action": "finished",
              "status": "ok"
            }
          ]
        }
        """

        let entries = try GatewayConnection.decodeCronRunsResponse(Data(json.utf8))

        #expect(entries.count == 1)
        #expect(entries.first?.jobId == "good")
    }
}
