import { assert, it } from "@effect/vitest";
import type { RepositoryIdentity } from "@t3tools/contracts";
import * as Clock from "effect/Clock";
import * as Deferred from "effect/Deferred";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as PubSub from "effect/PubSub";
import * as TestClock from "effect/testing/TestClock";
import * as Enrichment from "./ProjectEnrichmentService.ts";
import * as Favicon from "./ProjectFaviconResolver.ts";
import * as Repository from "./RepositoryIdentityResolver.ts";

// Evidence only: identical driver on both commits; assertions distinguish their expected outputs.
const after = process.env.EVIDENCE_SIDE === "after";
const identity = (root: string, version = 1): RepositoryIdentity => ({
  canonicalKey: `github.com/acme/${root.slice(1)}-v${version}`,
  rootPath: root,
  locator: { source: "git-remote", remoteName: "origin", remoteUrl: `https://github.com/acme/${root.slice(1)}-v${version}.git` },
});
const record = (row: string, step: string, value: unknown) => Effect.gen(function* () {
  const simulatedMillis = yield* Clock.currentTimeMillis;
  console.log(JSON.stringify({ row, side: process.env.EVIDENCE_SIDE, commit: process.env.EVIDENCE_SHA, simulatedMillis, step, value }));
});
const provide = (resolve: (root: string) => Effect.Effect<RepositoryIdentity | null>, capacity = 512) =>
  Layer.effect(Enrichment.ProjectEnrichmentService, Enrichment.make({cacheCapacity: capacity})).pipe(
    Layer.provide(Layer.merge(
      Layer.succeed(Repository.RepositoryIdentityResolver, {resolve}),
      Layer.succeed(Favicon.ProjectFaviconResolver, {resolvePath: () => Effect.succeed(null), invalidate: () => Effect.void}),
    )),
  );
const check = (value: Enrichment.ProjectEnrichment, root: string, version: number | null, resolved: boolean) => {
  assert.equal(value.repositoryIdentity?.canonicalKey ?? null, version === null ? null : identity(root, version).canonicalKey);
  assert.equal(value.repositoryIdentityResolved, resolved);
};

it.effect("F06 labelled harness: warmed identity, held failing refresh, failure TTL and recovery", () =>
  Effect.gen(function* () {
    const failedStarted = yield* Deferred.make<void>();
    const failRelease = yield* Deferred.make<void>();
    const recoveryStarted = yield* Deferred.make<void>();
    const recoveryRelease = yield* Deferred.make<void>();
    let calls = 0;
    const resolve = (root: string) => Effect.gen(function* () {
      calls++;
      if (calls === 2) {
        yield* Deferred.succeed(failedStarted, undefined);
        yield* Deferred.await(failRelease);
        return yield* Effect.die("synthetic resolver defect for F06");
      }
      if (calls === 3) {
        yield* Deferred.succeed(recoveryStarted, undefined);
        yield* Deferred.await(recoveryRelease);
      }
      return identity(root, calls === 1 ? 1 : 2);
    });
    yield* Effect.gen(function* () {
      const service = yield* Enrichment.ProjectEnrichmentService;
      const changes = yield* service.subscribeChanges;
      yield* record("F06", "cold", yield* service.peek("/widgets"));
      yield* service.request("/widgets");
      yield* record("F06", "warm-notification", yield* PubSub.take(changes));
      const warm = yield* service.peek("/widgets"); check(warm,"/widgets",1,true);
      yield* record("F06", "warm-I1", warm);
      yield* TestClock.adjust(Duration.seconds(61));
      const expired = yield* service.getAvailable("/widgets");check(expired,"/widgets",after?1:null,false);
      yield* record("F06", "expired-before-failing-refresh", expired);
      yield* Deferred.await(failedStarted);
      const pending = yield* service.peek("/widgets");check(pending,"/widgets",after?1:null,false);
      yield* record("F06", "failing-refresh-held", pending);
      yield* Deferred.succeed(failRelease,undefined);
      const failedEvent = yield* PubSub.take(changes);
      assert.isFalse(failedEvent.repositoryIdentityResolved);
      yield* record("F06", "failed-notification-unresolved", failedEvent);
      const failed = yield* service.peek("/widgets");check(failed,"/widgets",after?1:null,false);
      yield* record("F06", "failed-refresh-fallback", failed);
      const direct = yield* service.readRepositoryIdentity("/widgets");assert.isNull(direct);
      yield* record("F06", "direct-read-during-failure-still-null", direct);
      yield* TestClock.adjust(Duration.seconds(4));
      const cachedFailure = yield* service.getAvailable("/widgets");check(cachedFailure,"/widgets",after?1:null,false);
      assert.equal(calls,2);yield* record("F06", "four-seconds-into-failure-TTL-no-retry", {enrichment:cachedFailure,calls});
      yield* TestClock.adjust(Duration.seconds(2));
      yield* record("F06", "failure-TTL-expired-retry", yield* service.getAvailable("/widgets"));
      yield* Deferred.await(recoveryStarted);
      const recovering = yield* service.peek("/widgets");check(recovering,"/widgets",after?1:null,false);
      yield* record("F06", "recovery-held",recovering);
      yield* Deferred.succeed(recoveryRelease,undefined);
      yield* record("F06", "recovery-notification",yield* PubSub.take(changes));
      const recovered = yield* service.peek("/widgets");check(recovered,"/widgets",2,true);
      yield* record("F06", "recovered-I2",recovered);
      yield* TestClock.adjust(Duration.seconds(61));
      const later = yield* service.peek("/widgets");check(later,"/widgets",after?2:null,false);
      yield* record("F06", "I2-after-next-positive-TTL",later);
    }).pipe(Effect.provide(provide(resolve)));
  }),
);

it.effect("F13 labelled harness: capacity two, replacement order, eviction, pending refresh, null removal", () =>
  Effect.gen(function* () {
    const bStarted = yield* Deferred.make<void>();
    const bRelease = yield* Deferred.make<void>();
    const counts = new Map<string,number>();let nullC=false;
    const resolve = (root: string) => Effect.gen(function* () {
      const call=(counts.get(root)??0)+1;counts.set(root,call);
      if(root==="/B" && call>1){yield* Deferred.succeed(bStarted,undefined);yield* Deferred.await(bRelease);}
      return root==="/C" && nullC ? null : identity(root,call);
    });
    yield* Effect.gen(function* () {
      const service = yield* Enrichment.ProjectEnrichmentService;
      const changes = yield* service.subscribeChanges;
      yield* service.readRepositoryIdentity("/A");yield* service.readRepositoryIdentity("/B");
      yield* record("F13","warm-A-B",{A:yield* service.peek("/A"),B:yield* service.peek("/B")});
      yield* TestClock.adjust(Duration.seconds(61));
      yield* service.readRepositoryIdentity("/A");
      const bAfterReplace=yield* service.peek("/B");check(bAfterReplace,"/B",after?1:null,false);
      yield* record("F13","replace-A-moves-it-newest-without-evicting-B",{A:yield* service.peek("/A"),B:bAfterReplace});
      yield* service.readRepositoryIdentity("/C");
      yield* TestClock.adjust(Duration.seconds(61));
      const A=yield* service.peek("/A"),B=yield* service.peek("/B"),C=yield* service.peek("/C");
      check(A,"/A",after?2:null,false);check(B,"/B",null,false);check(C,"/C",after?1:null,false);
      yield* record("F13","overflow-C-evicts-oldest-B-not-recently-replaced-A",{cacheCapacity:2,A,B,C});
      yield* service.request("/B");yield* Deferred.await(bStarted);
      const pendingB=yield* service.peek("/B");check(pendingB,"/B",null,false);
      yield* record("F13","evicted-B-null-during-held-refresh-recent-C-retained",{B:pendingB,C:yield* service.peek("/C")});
      yield* Deferred.succeed(bRelease,undefined);yield* record("F13","B-refresh-completed",yield* PubSub.take(changes));
      nullC=true;assert.isNull(yield* service.readRepositoryIdentity("/C"));
      yield* service.readRepositoryIdentity("/D");yield* TestClock.adjust(Duration.seconds(61));
      const finalB=yield* service.peek("/B"), finalC=yield* service.peek("/C"), finalD=yield* service.peek("/D");
      check(finalB,"/B",after?2:null,false);check(finalC,"/C",null,false);check(finalD,"/D",after?1:null,false);
      yield* record("F13","resolved-null-C-clears-slot-so-B-and-D-survive",{B:finalB,C:finalC,D:finalD});
    }).pipe(Effect.provide(provide(resolve,2)));
  }),
);
