import { assert, describe, it } from "@effect/vitest"
import { Array, Context, Effect, Layer, Metric, Predicate, Ref } from "effect"
import { TestClock } from "effect/testing"
import { HttpClient, type HttpClientError, HttpClientResponse } from "effect/unstable/http"
import { OtlpMetrics, OtlpSerialization } from "effect/unstable/observability"

describe("OtlpMetrics", () => {
  describe("cumulative temporality", () => {
    it.effect("reports counter totals across export intervals", () =>
      Effect.gen(function*() {
        const metricName = "cumulative_counter_test"
        const counter = Metric.counter(metricName, {
          description: "Test counter"
        })

        // First interval: increment by 5
        yield* Metric.update(counter, 5)
        yield* triggerExport

        // Second interval: increment by 3 more (total: 8)
        yield* Metric.update(counter, 3)
        yield* triggerExport

        const requests = yield* MockHttpClient.requests
        assert.isAtLeast(requests.length, 2)

        const firstMetric = findMetric(requests[0], metricName)
        assert.strictEqual(firstMetric?.sum?.dataPoints[0].asDouble, 5)

        // Second export should report cumulative 8
        const secondMetric = findMetric(requests[1], metricName)
        assert.strictEqual(secondMetric?.sum?.dataPoints[0].asDouble, 8)
      }).pipe(Effect.provide(TestLayerCumulative)))

    it.effect("reports histogram count and sum across export intervals", () =>
      Effect.gen(function*() {
        const metricName = "cumulative_histogram_test"

        const histogram = Metric.histogram(metricName, {
          description: "Test histogram",
          boundaries: [10, 50, 100]
        })

        // First interval: observe values 25, 75 (count=2, sum=100)
        yield* Metric.update(histogram, 25)
        yield* Metric.update(histogram, 75)
        yield* triggerExport

        // Second interval: observe value 30 (cumulative count=3, sum=130)
        yield* Metric.update(histogram, 30)
        yield* triggerExport

        const requests = yield* MockHttpClient.requests
        assert.isAtLeast(requests.length, 2)

        const firstMetric = findMetric(requests[0], metricName)
        assert.strictEqual(firstMetric?.histogram?.dataPoints[0].count, 2)
        assert.strictEqual(firstMetric?.histogram?.dataPoints[0].sum, 100)

        const secondMetric = findMetric(requests[1], metricName)
        assert.isDefined(secondMetric)
        assert.strictEqual(secondMetric?.histogram?.dataPoints[0].count, 3)
        assert.strictEqual(secondMetric?.histogram?.dataPoints[0].sum, 130)
      }).pipe(Effect.provide(TestLayerCumulative)))

    it.effect("reports frequency counts across export intervals", () =>
      Effect.gen(function*() {
        const metricName = "cumulative_frequency_test"
        const frequency = Metric.frequency(metricName, {
          description: "Test frequency"
        })

        // First export interval: "a" x 2, "b" x 1
        yield* Metric.update(frequency, "a")
        yield* Metric.update(frequency, "a")
        yield* Metric.update(frequency, "b")
        yield* triggerExport

        // Second export interval: "a" x 1, "b" x 2 more (cumulative: "a"=3, "b"=3)
        yield* Metric.update(frequency, "a")
        yield* Metric.update(frequency, "b")
        yield* Metric.update(frequency, "b")
        yield* triggerExport

        const requests = yield* MockHttpClient.requests
        assert.isAtLeast(requests.length, 2)

        const firstMetric = findMetric(requests[0], metricName)
        const firstDataPoints = firstMetric?.sum?.dataPoints
        const firstA = firstDataPoints?.find((dp) =>
          dp.attributes.some((attr) =>
            attr.key === "key" &&
            Predicate.hasProperty(attr.value, "stringValue") &&
            attr.value.stringValue === "a"
          )
        )
        const firstB = firstDataPoints?.find((dp) =>
          dp.attributes.some((attr) =>
            attr.key === "key" &&
            Predicate.hasProperty(attr.value, "stringValue") &&
            attr.value.stringValue === "b"
          )
        )
        assert.strictEqual(firstA?.asInt, 2)
        assert.strictEqual(firstB?.asInt, 1)

        // Second export should report cumulative values
        const secondMetric = findMetric(requests[1], metricName)
        assert.isDefined(secondMetric)
        const secondDataPoints = secondMetric?.sum?.dataPoints
        const secondA = secondDataPoints?.find((dp) =>
          dp.attributes.some((attr) =>
            attr.key === "key" &&
            Predicate.hasProperty(attr.value, "stringValue") &&
            attr.value.stringValue === "a"
          )
        )
        const secondB = secondDataPoints?.find((dp) =>
          dp.attributes.some((attr) =>
            attr.key === "key" &&
            Predicate.hasProperty(attr.value, "stringValue") &&
            attr.value.stringValue === "b"
          )
        )
        assert.strictEqual(secondA?.asInt, 3) // Cumulative: 2+1=3
        assert.strictEqual(secondB?.asInt, 3) // Cumulative: 1+2=3
      }).pipe(Effect.provide(TestLayerCumulative)))

    it.effect("reports summary count and sum across export intervals", () =>
      Effect.gen(function*() {
        const metricName = "cumulative_summary_test"
        const summary = Metric.summary(metricName, {
          description: "Test summary",
          maxAge: "1 minute",
          maxSize: 100,
          quantiles: [0.5, 0.9, 0.99]
        })

        // First interval: observe 10, 20, 30 (count=3, sum=60)
        yield* Metric.update(summary, 10)
        yield* Metric.update(summary, 20)
        yield* Metric.update(summary, 30)
        yield* triggerExport

        // Second interval: observe 40 (cumulative count=4, sum=100)
        yield* Metric.update(summary, 40)
        yield* triggerExport

        const requests = yield* MockHttpClient.requests
        assert.isAtLeast(requests.length, 2)

        // First export - check count and sum metrics
        const firstCount = findMetric(requests[0], `${metricName}_count`)
        assert.strictEqual(firstCount?.sum?.dataPoints[0].asInt, 3)

        const firstSum = findMetric(requests[0], `${metricName}_sum`)
        assert.strictEqual(firstSum?.sum?.dataPoints[0].asDouble, 60)

        // Second export should report cumulative values
        const secondCount = findMetric(requests[1], `${metricName}_count`)
        assert.strictEqual(secondCount?.sum?.dataPoints[0].asInt, 4)

        const secondSum = findMetric(requests[1], `${metricName}_sum`)
        assert.strictEqual(secondSum?.sum?.dataPoints[0].asDouble, 100)
      }).pipe(Effect.provide(TestLayerCumulative)))
  })

  describe("delta temporality", () => {
    it.effect("reports counter deltas across export intervals", () =>
      Effect.gen(function*() {
        const metricName = "delta_counter_test"
        const counter = Metric.counter(metricName, {
          description: "Test counter"
        })

        // First export interval - increment the counter by 5
        yield* Metric.update(counter, 5)
        yield* triggerExport

        // Second export interval - increment the counter by 3 more (total: 8)
        yield* Metric.update(counter, 3)
        yield* triggerExport

        const requests = yield* MockHttpClient.requests
        assert.isAtLeast(requests.length, 2)

        const firstMetric = findMetric(requests[0], metricName)
        assert.strictEqual(firstMetric?.sum?.dataPoints[0].asDouble, 5)

        const secondMetric = findMetric(requests[1], metricName)
        assert.strictEqual(secondMetric?.sum?.dataPoints[0].asDouble, 3)
      }).pipe(Effect.provide(TestLayerDelta)))

    it.effect("reports histogram count and sum deltas across export intervals", () =>
      Effect.gen(function*() {
        const metricName = "delta_histogram_test"
        const histogram = Metric.histogram(metricName, {
          description: "Test histogram",
          boundaries: [10, 50, 100]
        })

        // First export interval: observe values 25, 75 (count=2, sum=100)
        yield* Metric.update(histogram, 25)
        yield* Metric.update(histogram, 75)
        yield* triggerExport

        // Second export interval: observe value 30 (delta count=1, delta sum=30)
        yield* Metric.update(histogram, 30)
        yield* triggerExport

        const requests = yield* MockHttpClient.requests
        assert.isAtLeast(requests.length, 2)

        const firstMetric = findMetric(requests[0], metricName)
        assert.strictEqual(firstMetric?.histogram?.dataPoints[0].count, 2)
        assert.strictEqual(firstMetric!.histogram!.dataPoints[0].sum, 100)

        const secondMetric = findMetric(requests[1], metricName)
        assert.strictEqual(secondMetric?.histogram?.dataPoints[0].count, 1)
        assert.strictEqual(secondMetric?.histogram?.dataPoints[0].sum, 30)
      }).pipe(Effect.provide(TestLayerDelta)))

    it.effect("reports frequency count deltas across export intervals", () =>
      Effect.gen(function*() {
        const metricName = "delta_frequency_test"
        const frequency = Metric.frequency(metricName, {
          description: "Test frequency"
        })

        // First export interval: "a" x 2, "b" x 1
        yield* Metric.update(frequency, "a")
        yield* Metric.update(frequency, "a")
        yield* Metric.update(frequency, "b")
        yield* triggerExport

        // Second export interval: "a" x 1, "b" x 2 more
        yield* Metric.update(frequency, "a")
        yield* Metric.update(frequency, "b")
        yield* Metric.update(frequency, "b")
        yield* triggerExport

        const requests = yield* MockHttpClient.requests
        assert.isAtLeast(requests.length, 2)

        const firstMetric = findMetric(requests[0], metricName)
        const firstDataPoints = firstMetric?.sum?.dataPoints
        const firstA = firstDataPoints?.find((dp) =>
          dp.attributes.some((attr) =>
            attr.key === "key" &&
            Predicate.hasProperty(attr.value, "stringValue") &&
            attr.value.stringValue === "a"
          )
        )
        const firstB = firstDataPoints?.find((dp) =>
          dp.attributes.some((attr) =>
            attr.key === "key" &&
            Predicate.hasProperty(attr.value, "stringValue") &&
            attr.value.stringValue === "b"
          )
        )
        assert.strictEqual(firstA?.asInt, 2)
        assert.strictEqual(firstB?.asInt, 1)

        const secondMetric = findMetric(requests[1], metricName)
        assert.isDefined(secondMetric)
        const secondDataPoints = secondMetric?.sum?.dataPoints
        const secondA = secondDataPoints?.find((dp) =>
          dp.attributes.some((attr) =>
            attr.key === "key" &&
            Predicate.hasProperty(attr.value, "stringValue") &&
            attr.value.stringValue === "a"
          )
        )
        const secondB = secondDataPoints?.find((dp) =>
          dp.attributes.some((attr) =>
            attr.key === "key" &&
            Predicate.hasProperty(attr.value, "stringValue") &&
            attr.value.stringValue === "b"
          )
        )
        assert.strictEqual(secondA?.asInt, 1) // Delta: 3-2=1
        assert.strictEqual(secondB?.asInt, 2) // Delta: 3-1=2
      }).pipe(Effect.provide(TestLayerDelta)))

    it.effect("reports summary count and sum deltas across export intervals", () =>
      Effect.gen(function*() {
        const metricName = "delta_summary_test"
        const summary = Metric.summary(metricName, {
          description: "Test summary",
          maxAge: "1 minute",
          maxSize: 100,
          quantiles: [0.5, 0.9, 0.99]
        })

        // First interval: observe 10, 20, 30 (count=3, sum=60)
        yield* Metric.update(summary, 10)
        yield* Metric.update(summary, 20)
        yield* Metric.update(summary, 30)
        yield* triggerExport

        // Second interval: observe 40 (delta count=1, delta sum=40)
        yield* Metric.update(summary, 40)
        yield* triggerExport

        const requests = yield* MockHttpClient.requests
        assert.isAtLeast(requests.length, 2)

        // First export
        const firstCount = findMetric(requests[0], `${metricName}_count`)
        assert.strictEqual(firstCount?.sum?.dataPoints[0].asInt, 3)

        const firstSum = findMetric(requests[0], `${metricName}_sum`)
        assert.strictEqual(firstSum?.sum?.dataPoints[0].asDouble, 60)

        // Second export should report delta values
        const secondCount = findMetric(requests[1], `${metricName}_count`)
        assert.strictEqual(secondCount?.sum?.dataPoints[0].asInt, 1) // Delta: 4-3=1

        const secondSum = findMetric(requests[1], `${metricName}_sum`)
        assert.strictEqual(secondSum?.sum?.dataPoints[0].asDouble, 40) // Delta: 100-60=40
      }).pipe(Effect.provide(TestLayerDelta)))
  })

  describe("Summary", () => {
    it.effect("exports quantile metrics", () =>
      Effect.gen(function*() {
        const metricName = "summary_quantiles_test"
        const summary = Metric.summary(metricName, {
          description: "Test summary",
          maxAge: "1 minute",
          maxSize: 100,
          quantiles: [0.5, 0.9]
        })

        yield* Metric.update(summary, 10)
        yield* Metric.update(summary, 20)
        yield* Metric.update(summary, 30)
        yield* triggerExport

        const requests = yield* MockHttpClient.requests
        assert.isAtLeast(requests.length, 1)

        // Check that quantiles metric exists
        const quantilesMetric = findMetric(requests[0], `${metricName}_quantiles`)
        assert.isDefined(quantilesMetric)
        assert.isDefined(quantilesMetric?.sum)

        // Should have data points for min, max, and each quantile (0.5, 0.9)
        const dataPoints = quantilesMetric?.sum?.dataPoints ?? []
        assert.isAtLeast(dataPoints.length, 4) // min, 0.5, 0.9, max

        // Find min and max quantile data points
        const minPoint = dataPoints.find((dp) =>
          dp.attributes.some((attr) =>
            attr.key === "quantile" &&
            Predicate.hasProperty(attr.value, "stringValue") &&
            attr.value.stringValue === "min"
          )
        )
        const maxPoint = dataPoints.find((dp) =>
          dp.attributes.some((attr) =>
            attr.key === "quantile" &&
            Predicate.hasProperty(attr.value, "stringValue") &&
            attr.value.stringValue === "max"
          )
        )
        assert.strictEqual(minPoint?.asDouble, 10)
        assert.strictEqual(maxPoint?.asDouble, 30)
      }).pipe(Effect.provide(TestLayerCumulative)))
  })

  describe("Gauge (no temporality)", () => {
    it.effect.each([
      ["cumulative", TestLayerCumulative] as const,
      ["delta", TestLayerDelta] as const
    ])("%s temporality reports current value", ([_, layer]) =>
      Effect.gen(function*() {
        const metricName = "delta_gauge_test"

        const gauge = Metric.gauge(metricName, {
          description: "Test gauge"
        })

        yield* Metric.update(gauge, 100)
        yield* triggerExport

        yield* Metric.update(gauge, 50)
        yield* triggerExport

        const requests = yield* MockHttpClient.requests
        assert.isAtLeast(requests.length, 2)

        // First export should report current value 100
        const firstMetric = findMetric(requests[0], metricName)
        assert.strictEqual(firstMetric?.gauge?.dataPoints[0].asDouble, 100)

        // Second export should report current value 50 (not delta -50)
        const secondMetric = findMetric(requests[1], metricName)
        assert.strictEqual(secondMetric?.gauge?.dataPoints[0].asDouble, 50)
      }).pipe(Effect.provide(layer)))
  })
})

interface OtlpExportRequest {
  readonly resourceMetrics: Array<{
    readonly resource?: unknown
    readonly scopeMetrics: Array<{
      readonly scope?: unknown | undefined
      readonly metrics: Array<OtlpMetric>
    }>
  }>
}

interface OtlpMetric {
  readonly name: string
  readonly description?: string | undefined
  readonly unit?: string | undefined
  readonly sum?: {
    readonly dataPoints: Array<OtlpNumberDataPoint>
    readonly aggregationTemporality: number
    readonly isMonotonic: boolean
  } | undefined
  readonly gauge?: {
    readonly dataPoints: Array<OtlpNumberDataPoint>
  } | undefined
  readonly histogram?: {
    readonly dataPoints: Array<OtlpHistogramDataPoint>
    readonly aggregationTemporality: number
  } | undefined
}

interface OtlpNumberDataPoint {
  readonly attributes: Array<{ key: string; value: unknown }>
  readonly startTimeUnixNano?: string | undefined
  readonly timeUnixNano?: string | undefined
  readonly asDouble?: number | undefined
  readonly asInt?: number | undefined
}

interface OtlpHistogramDataPoint {
  readonly attributes?: Array<{ key: string; value: unknown }>
  readonly startTimeUnixNano?: string | undefined
  readonly timeUnixNano?: string | undefined
  readonly count?: number | undefined
  readonly sum?: number | undefined
  readonly bucketCounts?: Array<number> | undefined
  readonly explicitBounds?: Array<number> | undefined
  readonly min?: number | undefined
  readonly max?: number | undefined
}

class MockHttpClient extends Context.Service<MockHttpClient, {
  readonly requests: Effect.Effect<ReadonlyArray<OtlpExportRequest>>
}>()("MockHttpClient") {
  static requests = Effect.service(MockHttpClient).pipe(
    Effect.flatMap((client) => client.requests)
  )
}

const makeHttpClient = Effect.gen(function*() {
  const capturedRequests = yield* Ref.make<ReadonlyArray<OtlpExportRequest>>([])

  const httpClient = HttpClient.makeWith(
    Effect.fnUntraced(function*(requestEffect) {
      const request = yield* requestEffect
      const body = (request.body._tag === "Uint8Array"
        ? JSON.parse(new TextDecoder().decode(request.body.body))
        : {}) as OtlpExportRequest
      yield* Ref.update(capturedRequests, Array.append(body))
      return HttpClientResponse.fromWeb(request, new Response())
    }),
    Effect.succeed as HttpClient.HttpClient.Preprocess<HttpClientError.HttpClientError, never>
  )

  return Context.make(HttpClient.HttpClient, httpClient).pipe(
    Context.add(MockHttpClient, MockHttpClient.of({ requests: Ref.get(capturedRequests) }))
  )
})

const HttpClientLayer = Layer.effectContext(makeHttpClient)

const OtlpCumulativeMetricsLayer = OtlpMetrics.layer({
  url: "http://localhost:4318/v1/metrics",
  resource: { serviceName: "test-service" },
  exportInterval: "10 seconds"
})

const OtlpDeltaMetricsLayer = OtlpMetrics.layer({
  url: "http://localhost:4318/v1/metrics",
  resource: { serviceName: "test-service" },
  temporality: "delta",
  exportInterval: "10 seconds"
})

const TestLayerCumulative = OtlpCumulativeMetricsLayer.pipe(
  Layer.provideMerge(HttpClientLayer),
  Layer.provide(OtlpSerialization.layerJson)
)

const TestLayerDelta = OtlpDeltaMetricsLayer.pipe(
  Layer.provideMerge(HttpClientLayer),
  Layer.provide(OtlpSerialization.layerJson)
)

const triggerExport = TestClock.adjust("10 seconds")

const findMetric = (request: OtlpExportRequest, name: string): OtlpMetric | undefined => {
  for (const resourceMetrics of request.resourceMetrics) {
    for (const scopeMetrics of resourceMetrics.scopeMetrics) {
      for (const metric of scopeMetrics.metrics) {
        if (metric.name === name) {
          return metric
        }
      }
    }
  }
  return undefined
}
