"""C1: run one fixed command script with unchanged redis-py against Redis 8 (ground truth, port 6379) and KiviDB (6380), RESP2 and RESP3."""
import redis, json, time, threading, sys
def N(v):
    if isinstance(v, bytes): return v.decode("latin-1")
    if isinstance(v, dict): return {str(N(k)): N(x) for k, x in sorted(v.items(), key=lambda kv: str(kv[0]))}
    if isinstance(v, (set, frozenset)): return sorted(N(x) for x in v)
    if isinstance(v, (list, tuple)): return [N(x) for x in v]
    if isinstance(v, (str, int, float, bool)) or v is None: return v
    return str(v)
def script(r):
    out = []
    def t(name, fn):
        try: v = fn()
        except Exception as e: v = f"ERR {type(e).__name__}: {str(e).split(' ')[0]}"
        out.append((name, N(v)))
    r.flushall()
    t("PING", lambda: r.ping()); t("ECHO", lambda: r.echo("hi"))
    t("SET", lambda: r.set("s", "v1")); t("GET", lambda: r.get("s")); t("GET missing", lambda: r.get("nope"))
    t("SET NX existing", lambda: r.set("s", "x", nx=True)); t("SET XX", lambda: r.set("s", "v2", xx=True)); t("GETSET", lambda: r.getset("s", "v3"))
    t("APPEND", lambda: r.append("s", "zz")); t("STRLEN", lambda: r.strlen("s")); t("GETRANGE", lambda: r.getrange("s", 0, 2)); t("SETRANGE", lambda: r.setrange("s", 1, "AB"))
    t("MSET", lambda: r.mset({"a": "1", "b": "2"})); t("MGET", lambda: r.mget("a", "b", "nope"))
    t("INCR", lambda: r.incr("n")); t("INCRBY", lambda: r.incrby("n", 10)); t("DECR", lambda: r.decr("n")); t("INCRBYFLOAT", lambda: r.incrbyfloat("f", 1.5))
    t("INCR non-int", lambda: r.incr("s"))
    t("EXPIRE", lambda: r.expire("a", 100)); t("TTL in range", lambda: 90 <= r.ttl("a") <= 100); t("PERSIST", lambda: r.persist("a")); t("TTL none", lambda: r.ttl("a")); t("TTL missing", lambda: r.ttl("nope"))
    t("SET PX + expiry works", lambda: (r.set("e", "1", px=150), time.sleep(0.3), r.exists("e"))[2])
    t("EXISTS", lambda: r.exists("a", "b", "nope")); t("DEL", lambda: r.delete("b", "nope")); t("TYPE string", lambda: r.type("s")); t("RENAME", lambda: r.rename("a", "a2")); t("TYPE missing", lambda: r.type("nope"))
    t("HSET", lambda: r.hset("h", mapping={"f1": "1", "f2": "2"})); t("HGET", lambda: r.hget("h", "f1")); t("HGETALL", lambda: r.hgetall("h")); t("HINCRBY", lambda: r.hincrby("h", "f1", 5))
    t("HDEL", lambda: r.hdel("h", "f2", "zz")); t("HLEN", lambda: r.hlen("h")); t("HEXISTS", lambda: r.hexists("h", "f1")); t("HKEYS", lambda: sorted(r.hkeys("h")))
    t("LPUSH", lambda: r.lpush("l", "a", "b")); t("RPUSH", lambda: r.rpush("l", "c")); t("LRANGE", lambda: r.lrange("l", 0, -1)); t("LPOP", lambda: r.lpop("l")); t("RPOP count", lambda: r.rpop("l", 2)); t("LLEN empty", lambda: r.llen("l"))
    t("LINDEX", lambda: (r.rpush("l2", "x", "y", "z"), r.lindex("l2", 1))[1]); t("LREM", lambda: r.lrem("l2", 0, "x")); t("LTRIM", lambda: r.ltrim("l2", 0, 0)); t("LRANGE after trim", lambda: r.lrange("l2", 0, -1))
    t("SADD", lambda: r.sadd("st", "a", "b", "c", "a")); t("SMEMBERS", lambda: r.smembers("st")); t("SISMEMBER", lambda: r.sismember("st", "b")); t("SREM", lambda: r.srem("st", "a")); t("SCARD", lambda: r.scard("st"))
    t("SINTER", lambda: (r.sadd("st2", "b", "z"), r.sinter("st", "st2"))[1]); t("SUNION", lambda: r.sunion("st", "st2")); t("SDIFF", lambda: r.sdiff("st", "st2"))
    t("ZADD", lambda: r.zadd("z", {"a": 1, "b": 2, "c": 3})); t("ZRANGE withscores", lambda: r.zrange("z", 0, -1, withscores=True)); t("ZSCORE", lambda: r.zscore("z", "b")); t("ZINCRBY", lambda: r.zincrby("z", 5, "a"))
    t("ZRANK", lambda: r.zrank("z", "a")); t("ZREVRANGE", lambda: r.zrevrange("z", 0, 1)); t("ZRANGEBYSCORE", lambda: r.zrangebyscore("z", 2, 3)); t("ZREM", lambda: r.zrem("z", "c")); t("ZCARD", lambda: r.zcard("z"))
    t("WRONGTYPE", lambda: r.lpush("s", "x"))
    t("binary-safe value", lambda: (r.set("bin", bytes(range(256))), r.get("bin") == bytes(range(256)))[1])
    t("SCAN all keys", lambda: sorted(k.decode() if isinstance(k, bytes) else k for k in r.scan_iter(count=10)))
    t("KEYS pattern", lambda: sorted(r.keys("l*"))); t("DBSIZE", lambda: r.dbsize())
    t("MULTI/EXEC", lambda: (lambda p: (p.set("tx", "1"), p.incr("tx"), p.get("tx"), p.execute())[3])(r.pipeline(transaction=True)))
    t("pipeline 100 INCR", lambda: r.pipeline(transaction=False).__enter__() and [r.pipeline(transaction=False)] and (lambda p: ([p.incr("pc") for _ in range(100)], p.execute()[-1])[1])(r.pipeline(transaction=False)))
    t("WATCH abort", lambda: _watch(r))
    t("pubsub", lambda: _pubsub(r))
    t("unknown command", lambda: r.execute_command("NOSUCHCMD"))
    t("SELECT db 1 isolation", lambda: (lambda r1: (r1.set("only1", "x"), r.exists("only1"), r1.flushdb())[1])(redis.Redis(port=r.connection_pool.connection_kwargs["port"], db=1, protocol=r.connection_pool.connection_kwargs.get("protocol", 2))))
    t("OBJECT ENCODING int", lambda: r.object("encoding", "n"))
    t("COPY", lambda: r.copy("s", "s_copy")); t("GETDEL", lambda: r.getdel("s_copy")); t("SETEX + TTL", lambda: (r.setex("sx", 50, "v"), 40 <= r.ttl("sx") <= 50)[1])
    return out
def _watch(r):
    r.set("w", "0"); p = r.pipeline(); p.watch("w"); r2 = redis.Redis(port=r.connection_pool.connection_kwargs["port"]); r2.set("w", "changed"); p.multi(); p.set("w", "mine")
    try: p.execute(); return "executed"
    except redis.WatchError: return "WatchError (aborted)"
def _pubsub(r):
    ps = r.pubsub(); ps.subscribe("ch"); time.sleep(0.2); n = r.publish("ch", "hello"); time.sleep(0.2); msgs = []
    for _ in range(5):
        m = ps.get_message(timeout=0.5)
        if m: msgs.append((m["type"], m["data"] if not isinstance(m["data"], bytes) else m["data"].decode()))
    ps.close(); return [n, msgs]
res = {}
for proto in (2, 3):
    for name, port in (("redis", 6379), ("kividb", 6380)):
        res[f"{name}_resp{proto}"] = script(redis.Redis(port=port, protocol=proto))
rows = []; summary = {}
for proto in (2, 3):
    a, b = res[f"redis_resp{proto}"], res[f"kividb_resp{proto}"]
    same = sum(1 for x, y in zip(a, b) if x == y); summary[f"resp{proto}"] = f"{same}/{len(a)} identical"
    rows += [{"proto": proto, "cmd": x[0], "redis": x[1], "kividb": y[1]} for x, y in zip(a, b) if x != y]
json.dump({"summary": summary, "differences": rows, "redis_py": redis.__version__, "all": res}, open(sys.argv[1], "w"), indent=1, ensure_ascii=False)
print(summary); [print(r) for r in rows]
