import { afterEach, describe, expect, it } from "vitest";
import { TestSqliteDatabase } from "./test-support.js";
import { RbacAccessRepository } from "../../../database/repositories/rbac-access-repository.js";

describe("RbacAccessRepository", () => {
  let adapter: TestSqliteDatabase | null = null;
  const activeAccessTime = "2026-06-26T12:00:00.000Z";

  afterEach(async () => {
    if (adapter) {
      await adapter.close();
      adapter = null;
    }
  });

  async function createRepository(
    onWrite?: () => void | Promise<void>,
  ): Promise<RbacAccessRepository> {
    adapter = new TestSqliteDatabase();
    const context = await adapter.connect();
    await adapter.exec(`
      INSERT INTO users (id, username, password_hash, is_admin, is_oidc)
      VALUES
        ('admin', 'admin', 'hash', 1, 0),
        ('user-1', 'alice', 'hash', 0, 0),
        ('owner-1', 'owner', 'hash', 0, 0);
      INSERT INTO roles (id, name, display_name, is_system)
      VALUES (7, 'ops', 'Operations', 0);
      INSERT INTO ssh_credentials (id, user_id, name, username, auth_type) VALUES
        (123, 'admin', 'cred-123', 'root', 'password'),
        (124, 'admin', 'cred-124', 'root', 'password'),
        (125, 'admin', 'cred-125', 'root', 'password'),
        (126, 'admin', 'cred-126', 'root', 'password');
      INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) VALUES
        (43, 'admin', 'host-43', '10.0.0.43', 22, 'root', 'password');
      INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) VALUES
        (44, 'admin', 'host-44', '10.0.0.45', 22, 'root', 'password');
      INSERT INTO ssh_data (
        id, user_id, name, ip, port, username, credential_id, rdp_credential_id, vnc_credential_id, telnet_credential_id, folder, tags, auth_type)
      VALUES (42, 'owner-1', 'prod', '10.0.0.42', 22, 'root', 123, 124, 125, 126, 'servers', 'linux', 'password');
      INSERT INTO snippets (id, user_id, name, content)
      VALUES
        (99, 'owner-1', 'deploy', 'echo deploy'),
        (100, 'owner-1', 'rollback', 'echo rollback');
      INSERT INTO host_access (
        id, host_id, user_id, role_id, granted_by, permission_level, expires_at, created_at
      )
      VALUES
        (1, 42, 'user-1', NULL, 'admin', 'view', NULL, '2026-06-26T00:00:00.000Z'),
        (2, 42, NULL, 7, 'admin', 'view', '2026-06-27T00:00:00.000Z', '2026-06-26T01:00:00.000Z'),
        (5, 44, 'user-1', NULL, 'admin', 'view', '2026-06-25T00:00:00.000Z', '2026-06-24T00:00:00.000Z');
      INSERT INTO snippet_access (
        id, snippet_id, user_id, role_id, granted_by, permission_level, expires_at, created_at
      )
      VALUES
        (3, 99, 'user-1', NULL, 'admin', 'view', NULL, '2026-06-26T00:00:00.000Z'),
        (4, 99, NULL, 7, 'admin', 'view', '2026-06-27T00:00:00.000Z', '2026-06-26T01:00:00.000Z');
      INSERT INTO shared_host_secrets (
        id, host_access_id, target_user_id, protocol, source_type, original_credential_id, encrypted_username, encrypted_auth_type
      )
      VALUES
        (8, 2, 'user-1', 'ssh', 'credential', 123, 'enc-user', 'enc-auth'),
        (9, 2, 'user-1', 'rdp', 'inline', NULL, 'enc-rdp-user', 'direct');
    `);

    return new RbacAccessRepository(context, onWrite);
  }

  it("lists host access with user and role target metadata", async () => {
    const repo = await createRepository();

    const accessList = await repo.listHostAccess(42);

    expect(accessList).toMatchObject([
      {
        id: 2,
        targetType: "role",
        userId: null,
        roleId: 7,
        username: null,
        roleName: "ops",
        roleDisplayName: "Operations",
        grantedByUsername: "admin",
      },
      {
        id: 1,
        targetType: "user",
        userId: "user-1",
        roleId: null,
        username: "alice",
        roleName: null,
        roleDisplayName: null,
        grantedByUsername: "admin",
      },
    ]);
  });

  it("lists snippet access with user and role target metadata", async () => {
    const repo = await createRepository();

    const accessList = await repo.listSnippetAccess(99);

    expect(accessList.map((access) => access.targetType)).toEqual([
      "role",
      "user",
    ]);
    expect(accessList[0]).toMatchObject({
      id: 4,
      roleId: 7,
      roleName: "ops",
      grantedByUsername: "admin",
    });
    expect(accessList[1]).toMatchObject({
      id: 3,
      userId: "user-1",
      username: "alice",
      grantedByUsername: "admin",
    });
  });

  it("lists shared hosts for direct and role access", async () => {
    const repo = await createRepository();

    const sharedHosts = await repo.listSharedHosts(
      "user-1",
      [7],
      activeAccessTime,
    );

    expect(sharedHosts).toMatchObject([
      {
        id: 42,
        name: "prod",
        ip: "10.0.0.42",
        ownerUsername: "owner",
        permissionLevel: "view",
      },
      {
        id: 42,
        name: "prod",
        ip: "10.0.0.42",
        ownerUsername: "owner",
        permissionLevel: "view",
      },
    ]);
  });

  it("lists visible host access entries for host list access checks", async () => {
    const repo = await createRepository();

    await expect(
      repo.listVisibleHostAccessEntries("user-1", [7], activeAccessTime),
    ).resolves.toEqual([
      {
        hostId: 42,
        permissionLevel: "view",
        expiresAt: "2026-06-27T00:00:00.000Z",
      },
      {
        hostId: 42,
        permissionLevel: "view",
        expiresAt: null,
      },
    ]);
  });

  it("lists role host access credential sources for role assignment", async () => {
    const repo = await createRepository();

    await expect(repo.listRoleHostAccessCredentialSources(7)).resolves.toEqual([
      {
        hostAccessId: 2,
        credentialId: 123,
        rdpCredentialId: 124,
        vncCredentialId: 125,
        telnetCredentialId: 126,
        hostId: 42,
        hostOwnerId: "owner-1",
      },
    ]);
  });

  it("finds shared secrets per protocol and host access owner", async () => {
    const repo = await createRepository();

    await expect(
      repo.findSharedSecretForHostUserProtocol(42, "user-1", "ssh"),
    ).resolves.toMatchObject({
      id: 8,
      hostAccessId: 2,
      protocol: "ssh",
      sourceType: "credential",
      originalCredentialId: 123,
      targetUserId: "user-1",
      encryptedUsername: "enc-user",
      encryptedAuthType: "enc-auth",
    });

    await expect(
      repo.findSharedSecretForHostUserProtocol(42, "user-1", "rdp"),
    ).resolves.toMatchObject({
      id: 9,
      protocol: "rdp",
      sourceType: "inline",
      originalCredentialId: null,
    });

    await expect(
      repo.findSharedSecretForHostUserProtocol(42, "user-1", "vnc"),
    ).resolves.toBeNull();
    await expect(
      repo.findSharedSecretForHostUserProtocol(99, "user-1", "ssh"),
    ).resolves.toBeNull();
    await expect(repo.findHostAccessOwnerId(2)).resolves.toBe("owner-1");
    await expect(repo.findHostAccessOwnerId(999)).resolves.toBeNull();
  });

  it("lists active grants, finds grants by id and updates grant level/expiry", async () => {
    let writeCount = 0;
    const repo = await createRepository(() => {
      writeCount += 1;
    });

    const grants = await repo.listActiveHostAccessGrants(42, activeAccessTime);
    expect(grants.map((grant) => grant.id).sort()).toEqual([1, 2]);

    // Host 44's only grant expired before activeAccessTime.
    await expect(
      repo.listActiveHostAccessGrants(44, activeAccessTime),
    ).resolves.toEqual([]);

    await expect(repo.findHostAccessById(1, 42)).resolves.toMatchObject({
      id: 1,
      hostId: 42,
      permissionLevel: "view",
    });
    await expect(repo.findHostAccessById(1, 99)).resolves.toBeNull();

    await expect(
      repo.updateHostAccessGrant(1, 42, {
        permissionLevel: "manage",
        expiresAt: "2026-07-01T00:00:00.000Z",
      }),
    ).resolves.toBe(true);
    await expect(repo.findHostAccessById(1, 42)).resolves.toMatchObject({
      permissionLevel: "manage",
      expiresAt: "2026-07-01T00:00:00.000Z",
    });

    await expect(
      repo.updateHostAccessGrant(999, 42, { permissionLevel: "view" }),
    ).resolves.toBe(false);
    expect(writeCount).toBe(1);
  });

  it("lists shared snippets and preserves route-level direct-over-role behavior", async () => {
    const repo = await createRepository();

    const sharedSnippets = await repo.listSharedSnippets(
      "user-1",
      [7],
      activeAccessTime,
    );

    expect(sharedSnippets).toHaveLength(1);
    expect(sharedSnippets[0]).toMatchObject({
      id: 99,
      name: "deploy",
      ownerUsername: "owner",
      permissionLevel: "view",
    });
  });

  it("lists visible shared snippets for the main snippets route", async () => {
    const repo = await createRepository();

    const sharedSnippets = await repo.listVisibleSharedSnippets(
      "user-1",
      [7],
      activeAccessTime,
    );

    expect(sharedSnippets.map((snippet) => snippet.id)).toEqual([99, 99]);
    expect(sharedSnippets[0]).toMatchObject({
      userId: "owner-1",
      name: "deploy",
      content: "echo deploy",
      ownerUsername: "owner",
    });
  });

  it("finds an accessible shared snippet for direct or role access", async () => {
    const repo = await createRepository();

    await expect(
      repo.findAccessibleSharedSnippet(99, "user-2", [7], activeAccessTime),
    ).resolves.toMatchObject({
      id: 99,
      userId: "owner-1",
      name: "deploy",
      content: "echo deploy",
      ownerUsername: "owner",
      permissionLevel: "view",
      hostFilter: null,
    });

    await expect(
      repo.findAccessibleSharedSnippet(
        99,
        "user-2",
        [7],
        "2026-06-28T00:00:00.000Z",
      ),
    ).resolves.toBeNull();
  });

  it("upserts host access and updates overrides", async () => {
    let writeCount = 0;
    const repo = await createRepository(() => {
      writeCount += 1;
    });

    const updated = await repo.upsertHostAccess({
      hostId: 42,
      targetType: "user",
      targetUserId: "user-1",
      grantedBy: "admin",
      permissionLevel: "view",
      expiresAt: "2026-06-28T00:00:00.000Z",
    });

    expect(updated).toEqual({ id: 1, created: false });
    expect(
      (await repo.listHostAccess(42)).find((row) => row.id === 1),
    ).toMatchObject({
      expiresAt: "2026-06-28T00:00:00.000Z",
    });

    const created = await repo.upsertHostAccess({
      hostId: 43,
      targetType: "role",
      targetRoleId: 7,
      grantedBy: "admin",
      permissionLevel: "view",
      expiresAt: null,
    });
    expect(created.created).toBe(true);

    const directAccess = await repo.findDirectHostAccess(42, "user-1");
    expect(directAccess?.id).toBe(1);

    await repo.touchHostAccess(1, "2026-06-26T03:00:00.000Z");
    expect(
      (await repo.findDirectHostAccess(42, "user-1"))?.lastAccessedAt,
    ).toBe("2026-06-26T03:00:00.000Z");

    await repo.revokeHostAccess(1, 42);
    expect(await repo.findDirectHostAccess(42, "user-1")).toBeNull();
    expect(writeCount).toBe(4);
  });

  it("finds active host access and deletes expired host access", async () => {
    let writeCount = 0;
    const repo = await createRepository(() => {
      writeCount += 1;
    });

    expect(
      await repo.findActiveHostAccess(
        42,
        "user-1",
        [7],
        "2026-06-26T12:00:00.000Z",
      ),
    ).toMatchObject({ id: 1 });
    expect(
      await repo.findActiveHostAccess(
        44,
        "user-1",
        [],
        "2026-06-26T12:00:00.000Z",
      ),
    ).toBeNull();

    expect(await repo.deleteExpiredHostAccess("2026-06-26T12:00:00.000Z")).toBe(
      1,
    );
    expect(await repo.findDirectHostAccess(44, "user-1")).toBeNull();
    expect(writeCount).toBe(1);
  });

  it("deletes host access for a host and only saves when rows changed", async () => {
    let writeCount = 0;
    const repo = await createRepository(() => {
      writeCount += 1;
    });

    expect(await repo.deleteHostAccessForHost(42)).toBe(2);
    expect(await repo.listHostAccess(42)).toEqual([]);
    expect(writeCount).toBe(1);

    expect(await repo.deleteHostAccessForHost(42)).toBe(0);
    expect(writeCount).toBe(1);
  });

  it("deletes host access for multiple hosts", async () => {
    let writeCount = 0;
    const repo = await createRepository(() => {
      writeCount += 1;
    });

    expect(await repo.deleteHostAccessForHosts([])).toBe(0);
    expect(writeCount).toBe(0);

    expect(await repo.deleteHostAccessForHosts([42, 44])).toBe(3);
    expect(await repo.listHostAccess(42)).toEqual([]);
    expect(await repo.findDirectHostAccess(44, "user-1")).toBeNull();
    expect(writeCount).toBe(1);
  });

  it("deletes host access that references a user directly or as grantor", async () => {
    let writeCount = 0;
    const repo = await createRepository(() => {
      writeCount += 1;
    });

    expect(await repo.deleteHostAccessForUserReferences("admin")).toBe(3);
    expect(await repo.listHostAccess(42)).toEqual([]);
    expect(await repo.findDirectHostAccess(44, "user-1")).toBeNull();
    expect(writeCount).toBe(1);

    expect(await repo.deleteHostAccessForUserReferences("admin")).toBe(0);
    expect(writeCount).toBe(1);
  });

  it("upserts and revokes snippet access", async () => {
    let writeCount = 0;
    const repo = await createRepository(() => {
      writeCount += 1;
    });

    const updated = await repo.upsertSnippetAccess({
      snippetId: 99,
      targetType: "user",
      targetUserId: "user-1",
      grantedBy: "admin",
      expiresAt: "2026-06-28T00:00:00.000Z",
    });

    expect(updated).toEqual({ id: 3, created: false });
    expect(
      (await repo.listSnippetAccess(99)).find((row) => row.id === 3),
    ).toMatchObject({ expiresAt: "2026-06-28T00:00:00.000Z" });

    const created = await repo.upsertSnippetAccess({
      snippetId: 100,
      targetType: "role",
      targetRoleId: 7,
      grantedBy: "admin",
      expiresAt: null,
    });
    expect(created.created).toBe(true);

    await repo.revokeSnippetAccess(3, 99);
    expect((await repo.listSnippetAccess(99)).map((row) => row.id)).toEqual([
      4,
    ]);
    expect(writeCount).toBe(3);
  });
});
