"""Tests for tRPC checkers (TRPC001-TRPC004)."""

import pytest
from pathlib import Path

from stablestack.checkers.trpc.procedure_no_input import TrpcProcedureNoInputChecker
from stablestack.checkers.trpc.inline_zod_schema import TrpcInlineZodSchemaChecker
from stablestack.checkers.trpc.result_type_bypass import TrpcResultTypeBypassChecker
from stablestack.checkers.trpc.missing_error_handling import TrpcMissingErrorHandlingChecker


# ── TRPC001: Procedure No Input ─────────────────────────────────────────


class TestTRPC001:
    def setup_method(self):
        self.checker = TrpcProcedureNoInputChecker()

    def test_metadata(self):
        assert self.checker.rule_id == "TRPC001"
        assert self.checker.rule_name == "trpc-procedure-no-input"

    def test_detects_input_without_validation(self, tmp_path):
        """Handler destructures input but chain has no .input()."""
        p = tmp_path / "router.ts"
        content = """\
export const userRouter = createTRPCRouter({
  getById: publicProcedure
    .query(async ({ ctx, input }) => {
      return ctx.prisma.user.findUnique({ where: { id: input } });
    }),
});
"""
        p.write_text(content)
        findings = self.checker.check_file(p, content)
        assert len(findings) == 1
        assert "no .input()" in findings[0].message

    def test_no_finding_with_input_validation(self, tmp_path):
        """Handler has .input() — no finding."""
        p = tmp_path / "router.ts"
        content = """\
export const userRouter = createTRPCRouter({
  getById: publicProcedure
    .input(z.string())
    .query(async ({ ctx, input }) => {
      return ctx.prisma.user.findUnique({ where: { id: input } });
    }),
});
"""
        p.write_text(content)
        findings = self.checker.check_file(p, content)
        assert len(findings) == 0

    def test_no_finding_without_input_destructure(self, tmp_path):
        """Handler only uses ctx (no input) — no finding."""
        p = tmp_path / "router.ts"
        content = """\
export const userRouter = createTRPCRouter({
  getAll: publicProcedure
    .query(async ({ ctx }) => {
      return ctx.prisma.user.findMany();
    }),
});
"""
        p.write_text(content)
        findings = self.checker.check_file(p, content)
        assert len(findings) == 0

    def test_mutation_without_input(self, tmp_path):
        """Mutation handler with input but no .input() — finding."""
        p = tmp_path / "router.ts"
        content = """\
export const userRouter = createTRPCRouter({
  delete: publicProcedure
    .mutation(async ({ ctx, input }) => {
      return ctx.prisma.user.delete({ where: { id: input } });
    }),
});
"""
        p.write_text(content)
        findings = self.checker.check_file(p, content)
        assert len(findings) == 1
        assert ".mutation()" in findings[0].message

    def test_nested_procedure_with_middleware(self, tmp_path):
        """Procedure chain with middleware but no .input() — finding."""
        p = tmp_path / "router.ts"
        content = """\
export const adminRouter = createTRPCRouter({
  deleteUser: adminProcedure
    .use(logMiddleware)
    .mutation(async ({ ctx, input }) => {
      return ctx.prisma.user.delete({ where: { id: input } });
    }),
});
"""
        p.write_text(content)
        findings = self.checker.check_file(p, content)
        assert len(findings) == 1

    def test_middleware_with_input(self, tmp_path):
        """Procedure chain with middleware AND .input() — no finding."""
        p = tmp_path / "router.ts"
        content = """\
export const adminRouter = createTRPCRouter({
  deleteUser: adminProcedure
    .use(logMiddleware)
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
      return ctx.prisma.user.delete({ where: { id: input.id } });
    }),
});
"""
        p.write_text(content)
        findings = self.checker.check_file(p, content)
        assert len(findings) == 0


# ── TRPC002: Inline Zod Schema ──────────────────────────────────────────


class TestTRPC002:
    def setup_method(self):
        self.checker = TrpcInlineZodSchemaChecker()

    def test_metadata(self):
        assert self.checker.rule_id == "TRPC002"
        assert self.checker.is_project_checker is True

    def test_detects_inline_zod_in_monorepo(self, tmp_path):
        """Inline z.object() in .input() within a monorepo — finding."""
        # Create monorepo structure
        (tmp_path / "packages" / "shared").mkdir(parents=True)
        router_dir = tmp_path / "packages" / "backend" / "src"
        router_dir.mkdir(parents=True)
        router = router_dir / "router.ts"
        router.write_text("""\
export const userRouter = createTRPCRouter({
  create: publicProcedure
    .input(z.object({ name: z.string(), email: z.string() }))
    .mutation(async ({ ctx, input }) => {
      return ctx.prisma.user.create({ data: input });
    }),
});
""")
        findings = self.checker.check_project(tmp_path)
        assert len(findings) == 1
        assert "Inline Zod schema" in findings[0].message

    def test_no_finding_with_imported_schema(self, tmp_path):
        """Imported schema identifier in .input() — no finding."""
        (tmp_path / "packages" / "shared").mkdir(parents=True)
        router_dir = tmp_path / "packages" / "backend" / "src"
        router_dir.mkdir(parents=True)
        router = router_dir / "router.ts"
        router.write_text("""\
import { createUserSchema } from '@shared/schemas';

export const userRouter = createTRPCRouter({
  create: publicProcedure
    .input(createUserSchema)
    .mutation(async ({ ctx, input }) => {
      return ctx.prisma.user.create({ data: input });
    }),
});
""")
        findings = self.checker.check_project(tmp_path)
        assert len(findings) == 0

    def test_no_finding_outside_monorepo(self, tmp_path):
        """Inline schema without packages/shared — no finding (not a monorepo)."""
        src_dir = tmp_path / "src"
        src_dir.mkdir()
        router = src_dir / "router.ts"
        router.write_text("""\
export const userRouter = createTRPCRouter({
  create: publicProcedure
    .input(z.object({ name: z.string() }))
    .mutation(async ({ ctx, input }) => {
      return ctx.prisma.user.create({ data: input });
    }),
});
""")
        findings = self.checker.check_project(tmp_path)
        assert len(findings) == 0

    def test_inline_z_array(self, tmp_path):
        """Inline z.array() in .input() — also flagged."""
        (tmp_path / "packages" / "shared").mkdir(parents=True)
        router_dir = tmp_path / "packages" / "backend" / "src"
        router_dir.mkdir(parents=True)
        router = router_dir / "router.ts"
        router.write_text("""\
export const userRouter = createTRPCRouter({
  bulkDelete: publicProcedure
    .input(z.array(z.string()))
    .mutation(async ({ ctx, input }) => {
      return ctx.prisma.user.deleteMany({ where: { id: { in: input } } });
    }),
});
""")
        findings = self.checker.check_project(tmp_path)
        assert len(findings) == 1


# ── TRPC003: Result Type Bypass ──────────────────────────────────────────


class TestTRPC003:
    def setup_method(self):
        self.checker = TrpcResultTypeBypassChecker()

    def test_metadata(self):
        assert self.checker.rule_id == "TRPC003"
        assert self.checker.rule_name == "trpc-result-type-bypass"

    def test_detects_as_any_on_useQuery(self, tmp_path):
        """useQuery result cast to `as any` — finding."""
        p = tmp_path / "component.tsx"
        content = """\
const data = trpc.user.getById.useQuery({ id: "123" }) as any;
"""
        p.write_text(content)
        findings = self.checker.check_file(p, content)
        assert len(findings) == 1
        assert "type bypass" in findings[0].message.lower() or "Type bypass" in findings[0].message

    def test_detects_as_unknown_as_on_data(self, tmp_path):
        """`.data as unknown as User` — finding."""
        p = tmp_path / "component.tsx"
        content = """\
const { data } = trpc.user.getById.useQuery({ id: "123" });
const user = data.data as unknown as User;
"""
        p.write_text(content)
        # This should detect the .data as unknown as pattern in tRPC context
        findings = self.checker.check_file(p, content)
        assert len(findings) >= 1

    def test_detects_ts_ignore_on_trpc_line(self, tmp_path):
        """@ts-ignore above a tRPC line — finding."""
        p = tmp_path / "component.tsx"
        content = """\
// @ts-ignore
const data = trpc.user.getById.useQuery({ id: "123" });
"""
        p.write_text(content)
        findings = self.checker.check_file(p, content)
        assert len(findings) == 1
        assert "suppression" in findings[0].message.lower() or "ts-ignore" in findings[0].fix_hint.lower()

    def test_detects_ts_expect_error_on_trpc(self, tmp_path):
        """@ts-expect-error above tRPC — finding."""
        p = tmp_path / "component.tsx"
        content = """\
// @ts-expect-error
const mutation = trpc.user.delete.useMutation();
"""
        p.write_text(content)
        findings = self.checker.check_file(p, content)
        assert len(findings) == 1

    def test_no_finding_on_normal_trpc_usage(self, tmp_path):
        """Normal tRPC usage — no finding."""
        p = tmp_path / "component.tsx"
        content = """\
const { data } = trpc.user.getById.useQuery({ id: "123" });
const mutation = trpc.user.create.useMutation({
  onSuccess: () => router.push("/users"),
});
"""
        p.write_text(content)
        findings = self.checker.check_file(p, content)
        assert len(findings) == 0

    def test_as_any_on_useMutation(self, tmp_path):
        """useMutation cast to as any — finding."""
        p = tmp_path / "component.tsx"
        content = """\
const mutation = trpc.user.create.useMutation() as any;
"""
        p.write_text(content)
        findings = self.checker.check_file(p, content)
        assert len(findings) == 1


# ── TRPC004: Missing Error Handling ──────────────────────────────────────


class TestTRPC004:
    def setup_method(self):
        self.checker = TrpcMissingErrorHandlingChecker()

    def test_metadata(self):
        assert self.checker.rule_id == "TRPC004"
        assert self.checker.rule_name == "trpc-missing-error-handling"

    def test_detects_mutate_without_try_catch(self, tmp_path):
        """Direct .mutate() without try-catch — finding."""
        p = tmp_path / "component.tsx"
        content = """\
async function deleteUser(id: string) {
  await trpc.user.delete.mutate({ id });
  router.push("/users");
}
"""
        p.write_text(content)
        findings = self.checker.check_file(p, content)
        assert len(findings) == 1
        assert ".mutate()" in findings[0].message

    def test_no_finding_mutate_in_try_catch(self, tmp_path):
        """.mutate() inside try-catch — no finding."""
        p = tmp_path / "component.tsx"
        content = """\
async function deleteUser(id: string) {
  try {
    await trpc.user.delete.mutate({ id });
    router.push("/users");
  } catch (err) {
    toast.error("Failed to delete user");
  }
}
"""
        p.write_text(content)
        findings = self.checker.check_file(p, content)
        assert len(findings) == 0

    def test_detects_useMutation_without_onError(self, tmp_path):
        """useMutation() without onError — finding."""
        p = tmp_path / "component.tsx"
        content = """\
const mutation = trpc.user.delete.useMutation();
"""
        p.write_text(content)
        findings = self.checker.check_file(p, content)
        assert len(findings) == 1
        assert "onError" in findings[0].message

    def test_detects_useMutation_empty_options(self, tmp_path):
        """useMutation({}) without onError — finding."""
        p = tmp_path / "component.tsx"
        content = """\
const mutation = trpc.user.delete.useMutation({
  onSuccess: () => router.push("/users"),
});
"""
        p.write_text(content)
        findings = self.checker.check_file(p, content)
        assert len(findings) == 1

    def test_no_finding_useMutation_with_onError(self, tmp_path):
        """useMutation with onError — no finding."""
        p = tmp_path / "component.tsx"
        content = """\
const mutation = trpc.user.delete.useMutation({
  onError: (err) => toast.error(err.message),
  onSuccess: () => router.push("/users"),
});
"""
        p.write_text(content)
        findings = self.checker.check_file(p, content)
        assert len(findings) == 0

    def test_no_finding_useMutation_with_catch(self, tmp_path):
        """useMutation with .catch() chained — no finding."""
        p = tmp_path / "component.tsx"
        content = """\
const mutation = trpc.user.delete.useMutation().catch(handleError);
"""
        p.write_text(content)
        # The .catch( appears in the collected text
        findings = self.checker.check_file(p, content)
        assert len(findings) == 0
