import Testing
import Foundation
@testable import WhatCableCore

@Suite("Cable Report")
struct CableReportTests {

    private func cableIdentity(
        vendorID: Int = 0x05AC,
        productID: Int = 0x1234,
        endpoint: USBPDSOP.Endpoint = .sopPrime,
        vdos: [UInt32] = [
            // ID Header VDO: passive cable from VID 0x05AC
            (3 << 27) | UInt32(0x05AC),
            0,
            0,
            // Cable VDO: USB4 Gen 3 (0b011), 5A (0b10), passive,
            // latency 0001 (~1 m). A bare-zero VDO would trip the
            // reservedCableLatencyEncoding warning even though these
            // tests aren't about trust signals.
            (0b10 << 5) | 0b011 | (1 << 13)
        ]
    ) -> USBPDSOP {
        USBPDSOP(
            id: 1,
            endpoint: endpoint,
            parentPortType: 0,
            parentPortNumber: 0,
            vendorID: vendorID,
            productID: productID,
            bcdDevice: 0,
            vdos: vdos,
            specRevision: 3
        )
    }

    @Test("Payload only built for cable endpoints")
    func payloadOnlyBuiltForCableEndpoints() {
        #expect(CableReport.payload(for: cableIdentity(endpoint: .sopPrime)) != nil)
        #expect(CableReport.payload(for: cableIdentity(endpoint: .sopDoublePrime)) != nil)
        #expect(CableReport.payload(for: cableIdentity(endpoint: .sop)) == nil)
        #expect(CableReport.payload(for: cableIdentity(endpoint: .unknown)) == nil)
    }

    @Test("Fingerprint formats hex as uppercase four digits")
    func fingerprintFormatsHexAsUppercaseFourDigits() {
        let payload = CableReport.payload(for: cableIdentity(vendorID: 0x05AC, productID: 0x004C))!
        #expect(payload.cable.vendorIDHex == "0x05AC")
        #expect(payload.cable.productIDHex == "0x004C")
    }

    @Test("Fingerprint labels unregistered vendor")
    func fingerprintLabelsUnregisteredVendor() {
        let payload = CableReport.payload(for: cableIdentity(vendorID: 0xDEAD))!
        #expect(payload.cable.vendorName == "Unregistered / unknown")
    }

    @Test("Markdown includes fingerprint and environment")
    func markdownIncludesFingerprintAndEnvironment() {
        let payload = CableReport.payload(for: cableIdentity(), appVersion: "1.2.3")!
        let md = payload.markdown
        #expect(md.contains("### Cable e-marker fingerprint"))
        #expect(md.contains("`0x05AC`"))
        #expect(md.contains("Apple"))
        #expect(md.contains("### Environment"))
        #expect(md.contains("WhatCable: `1.2.3`"))
        // No system info opt-in: should be flagged as not included.
        #expect(md.contains("not included by reporter"))
    }

    @Test("Markdown includes system info when provided")
    func markdownIncludesSystemInfoWhenProvided() {
        let payload = CableReport.Payload(
            cable: CableReport.CableFingerprint(identity: cableIdentity()),
            system: CableReport.SystemInfo(macModel: "Mac15,3", macOSVersion: "14.5.0"),
            appVersion: "1.2.3"
        )
        let md = payload.markdown
        #expect(md.contains("Mac: `Mac15,3`"))
        #expect(md.contains("macOS: `14.5.0`"))
        #expect(md.contains("not included by reporter") == false)
    }

    @Test("GitHub URL targets template and carries fingerprint")
    func gitHubURLTargetsTemplateAndCarriesFingerprint() throws {
        let payload = CableReport.payload(for: cableIdentity())!
        let url = payload.githubURL
        let comps = try #require(URLComponents(url: url, resolvingAgainstBaseURL: false))
        #expect(comps.host == "github.com")
        #expect(comps.path == "/darrylmorley/whatcable/issues/new")
        let items = Dictionary(uniqueKeysWithValues:
            (comps.queryItems ?? []).map { ($0.name, $0.value ?? "") }
        )
        #expect(items["template"] == "cable-report.yml")
        #expect(items["labels"] == "cable-report")
        #expect(items["title"]?.hasPrefix("[Cable Report]") == true)
        #expect(items["fingerprint"]?.contains("0x05AC") == true)
    }

    @Test("Issue title includes vendor and speed")
    func issueTitleIncludesVendorAndSpeed() {
        let payload = CableReport.payload(for: cableIdentity())!
        #expect(payload.issueTitle.contains("Apple"))
        #expect(payload.issueTitle.contains("USB4"))
    }

    @Test("Fingerprint carries raw VDOs")
    func fingerprintCarriesRawVDOs() {
        let payload = CableReport.payload(for: cableIdentity())!
        // Fixture has 4 VDOs: ID Header, Cert Stat, Product, Cable.
        #expect(payload.cable.vdos.count == 4)
        // VDO[0] = passive cable header (3 << 27) | 0x05AC.
        #expect(payload.cable.vdos[0] == (3 << 27) | UInt32(0x05AC))
    }

    @Test("Markdown includes raw VDO section")
    func markdownIncludesRawVDOSection() {
        let payload = CableReport.payload(for: cableIdentity())!
        let md = payload.markdown
        #expect(md.contains("### Raw VDOs"))
        // ID Header VDO from the fixture: (3 << 27) | 0x05AC = 0x180005AC.
        #expect(md.contains("`0x180005AC`"))
        // Role labels appear so future readers can tell which is which
        // without having to know the spec layout.
        #expect(md.contains("ID Header"))
        #expect(md.contains("Cable"))
        #expect(md.contains("Product"))
    }

    @Test("Markdown omits raw VDO section when absent")
    func markdownOmitsRawVDOSectionWhenAbsent() {
        // Identity with no VDOs (e.g. a cable that didn't respond to
        // Discover Identity at all) shouldn't render an empty Raw VDOs table.
        let id = USBPDSOP(
            id: 1,
            endpoint: .sopPrime,
            parentPortType: 0,
            parentPortNumber: 0,
            vendorID: 0x05AC,
            productID: 0,
            bcdDevice: 0,
            vdos: [],
            specRevision: 3
        )
        let payload = CableReport.payload(for: id)!
        let md = payload.markdown
        #expect(md.contains("### Raw VDOs") == false)
    }

    // MARK: - USB-IF certification ID (from Cert Stat VDO)

    @Test("USB-IF cert ID present when non-zero")
    func usbIFCertIDPresentWhenNonZero() {
        let id = USBPDSOP(
            id: 1,
            endpoint: .sopPrime,
            parentPortType: 0,
            parentPortNumber: 0,
            vendorID: 0x05AC,
            productID: 0x1234,
            bcdDevice: 0,
            vdos: [
                (3 << 27) | UInt32(0x05AC),
                0x00012345,             // Cert Stat with XID
                0,
                (0b10 << 5) | 0b011 | (1 << 13)
            ],
            specRevision: 3
        )
        let payload = CableReport.payload(for: id)!
        #expect(payload.cable.usbifCertID == 0x00012345)
        let md = payload.markdown
        #expect(md.contains("USB-IF certification ID"))
        #expect(md.contains("0x00012345"))
    }

    @Test("USB-IF cert ID absent when zero")
    func usbIFCertIDAbsentWhenZero() {
        // Calibration: Anker #60 and Caldigit #62 both ship with XID = 0.
        // We surface that as "none" rather than a trust signal.
        let payload = CableReport.payload(for: cableIdentity())!
        #expect(payload.cable.usbifCertID == nil)
        let md = payload.markdown
        #expect(md.contains("USB-IF certification ID"))
        #expect(md.contains("none (XID = 0)"))
    }

    @Test("USB-IF cert ID distinguishes absent VDO from zero value")
    func usbIFCertIDDistinguishesAbsentVDOFromZeroValue() {
        // Identity with only an ID Header VDO -- macOS didn't surface a
        // Cert Stat. The fingerprint should record that explicitly,
        // not flatten it to "XID = 0", so calibration data stays
        // faithful to what the cable actually reported.
        let id = USBPDSOP(
            id: 1,
            endpoint: .sopPrime,
            parentPortType: 0,
            parentPortNumber: 0,
            vendorID: 0x05AC,
            productID: 0,
            bcdDevice: 0,
            vdos: [
                (3 << 27) | UInt32(0x05AC) // only ID Header, no Cert Stat
            ],
            specRevision: 3
        )
        let payload = CableReport.payload(for: id)!
        #expect(payload.cable.usbifCertID == nil)
        let md = payload.markdown
        #expect(md.contains("USB-IF certification ID"))
        #expect(md.contains("not provided by this Mac"))
        #expect(
            md.contains("none (XID = 0)") == false,
            "Missing VDO[1] must not be rendered the same as a real zero XID"
        )
    }

    // MARK: - CIO Thunderbolt link context

    @Test("Markdown includes CIO section when present")
    func markdownIncludesCIOSectionWhenPresent() {
        let cio = CIOCableCapability(
            id: 1,
            portKey: "2/0",
            cableGeneration: 2,
            cableSpeed: 3,
            generation: 3,
            asymmetricModeSupported: true,
            legacyAdapter: false,
            linkTrainingMode: 2
        )
        let payload = CableReport.payload(
            for: cableIdentity(),
            cioCapability: cio
        )!
        let md = payload.markdown
        #expect(md.contains("### Thunderbolt link context"))
        #expect(md.contains("CableGeneration"))
        #expect(md.contains("| `2` |"))
        #expect(md.contains("CableSpeed"))
        #expect(md.contains("| `3` |"))
        #expect(md.contains("Generation"))
        #expect(md.contains("AsymmetricModeSupported"))
        #expect(md.contains("| Yes |"))
        #expect(md.contains("LegacyAdapter"))
        #expect(md.contains("| No |"))
        #expect(md.contains("LinkTrainingMode"))
    }

    @Test("Markdown omits CIO section when absent")
    func markdownOmitsCIOSectionWhenAbsent() {
        let payload = CableReport.payload(for: cableIdentity())!
        let md = payload.markdown
        #expect(md.contains("### Thunderbolt link context") == false)
        #expect(md.contains("CableGeneration") == false)
    }

    @Test("CIO section omitted when all fields nil")
    func cioSectionOmittedWhenAllFieldsNil() {
        let cio = CIOCableCapability(
            id: 1,
            portKey: "2/0",
            cableGeneration: nil,
            cableSpeed: nil,
            generation: nil,
            asymmetricModeSupported: nil,
            legacyAdapter: nil,
            linkTrainingMode: nil
        )
        let payload = CableReport.payload(
            for: cableIdentity(),
            cioCapability: cio
        )!
        let md = payload.markdown
        #expect(
            md.contains("### Thunderbolt link context") == false,
            "All-nil CIO should not render an empty table"
        )
    }

    @Test("CIO section omits nil fields")
    func cioSectionOmitsNilFields() {
        // CIO with only cableSpeed set, everything else nil.
        let cio = CIOCableCapability(
            id: 1,
            portKey: "2/0",
            cableGeneration: nil,
            cableSpeed: 3,
            generation: nil,
            asymmetricModeSupported: nil,
            legacyAdapter: nil,
            linkTrainingMode: nil
        )
        let payload = CableReport.payload(
            for: cableIdentity(),
            cioCapability: cio
        )!
        let md = payload.markdown
        #expect(md.contains("### Thunderbolt link context"))
        #expect(md.contains("CableSpeed"))
        #expect(md.contains("CableGeneration") == false)
        #expect(md.contains("AsymmetricModeSupported") == false)
        #expect(md.contains("LinkTrainingMode") == false)
    }

    @Test("Markdown labels extra VDOs as Other")
    func markdownLabelsExtraVDOsAsOther() {
        // PD response can include up to 7 VDOs (ID Header + Cert Stat +
        // Product + up to 4 Product Type VDOs). Anything past index 3 we
        // label "Other" rather than guessing.
        let id = USBPDSOP(
            id: 1,
            endpoint: .sopPrime,
            parentPortType: 0,
            parentPortNumber: 0,
            vendorID: 0x05AC,
            productID: 0x1234,
            bcdDevice: 0,
            vdos: [
                (3 << 27) | UInt32(0x05AC),
                0,
                0,
                (0b10 << 5) | 0b011 | (1 << 13), // valid 1m latency
                0xDEADBEEF,
                0xCAFEBABE
            ],
            specRevision: 3
        )
        let payload = CableReport.payload(for: id)!
        let md = payload.markdown
        #expect(md.contains("`0xDEADBEEF`"))
        #expect(md.contains("`0xCAFEBABE`"))
        #expect(md.contains("Other"))
    }
}
