//! The Adblock Plus filter syntax, restricted to what a DNS sinkhole can
//! honour: domain anchors and bare names. Pure, `std` only.
//!
//! Exception rules (`@@`) are `.unsupported` rather than an allow entry. The
//! allow surface is the `rules` table, and a downloaded list that could quietly
//! allow a domain across every group is a policy hole the operator did not open.

const std = @import("std");
const parsers = @import("parsers.zig");

/// Tokens that a candidate name may never contain. `^` is a separator token in
/// this syntax and only a trailing one is meaningful for a domain rule.
const rule_tokens = "*^|/$";

pub fn parseLine(line: []const u8) parsers.Line {
    const text = std.mem.trim(u8, line, &std.ascii.whitespace);
    if (text.len == 0) return .{ .kind = .ignore };
    if (text[0] == '!') return .{ .kind = .ignore };
    if (text[0] == '[') return .{ .kind = .ignore };
    if (parsers.isElementHiding(text)) return .{ .kind = .unsupported };
    if (text[0] == '#') return .{ .kind = .ignore };
    if (std.mem.startsWith(u8, text, "@@")) return .{ .kind = .unsupported };
    if (text[0] == '/') return .{ .kind = .regex };
    if (std.mem.findScalar(u8, text, '$') != null) return .{ .kind = .unsupported };

    if (std.mem.startsWith(u8, text, "||")) {
        var candidate = text[2..];
        if (std.mem.endsWith(u8, candidate, "^")) candidate = candidate[0 .. candidate.len - 1];
        if (candidate.len == 0) return .{ .kind = .unsupported };
        if (std.mem.findAny(u8, candidate, rule_tokens) != null) return .{ .kind = .unsupported };
        // A domain anchor covers the domain itself as well as its subdomains,
        // so the compiler emits an apex entry beside the wildcard one.
        return .{ .kind = .wildcard, .text = candidate, .covers_apex = true };
    }

    if (std.mem.findAny(u8, text, rule_tokens) != null) return .{ .kind = .unsupported };
    return .{ .kind = .domain, .text = text };
}

const testing = std.testing;

test "a bang comment is ignored" {
    try testing.expectEqual(parsers.Kind.ignore, parseLine("! comment").kind);
}

test "a list header is ignored" {
    try testing.expectEqual(parsers.Kind.ignore, parseLine("[Adblock Plus 2.0]").kind);
}

test "an empty line is ignored" {
    try testing.expectEqual(parsers.Kind.ignore, parseLine("").kind);
}

test "a domain anchor is a wildcard that covers its apex" {
    const line = parseLine("||example.com^");
    try testing.expectEqual(parsers.Kind.wildcard, line.kind);
    try testing.expectEqualStrings("example.com", line.text);
    try testing.expect(line.covers_apex);
}

test "a domain anchor without a separator is still a wildcard" {
    const line = parseLine("||example.com");
    try testing.expectEqual(parsers.Kind.wildcard, line.kind);
    try testing.expectEqualStrings("example.com", line.text);
    try testing.expect(line.covers_apex);
}

test "a modifier list is unsupported" {
    try testing.expectEqual(parsers.Kind.unsupported, parseLine("||example.com^$third-party").kind);
}

test "an exception rule is unsupported" {
    try testing.expectEqual(parsers.Kind.unsupported, parseLine("@@||example.com^").kind);
}

test "element hiding is unsupported" {
    try testing.expectEqual(parsers.Kind.unsupported, parseLine("##.ad-banner").kind);
    try testing.expectEqual(parsers.Kind.unsupported, parseLine("example.com#@#.ad").kind);
    try testing.expectEqual(parsers.Kind.unsupported, parseLine("example.com#?#.ad").kind);
}

test "a scheme anchor is unsupported" {
    try testing.expectEqual(parsers.Kind.unsupported, parseLine("|http://example.com").kind);
    try testing.expectEqual(parsers.Kind.unsupported, parseLine("|https://example.com").kind);
}

test "a regex rule is recognized" {
    try testing.expectEqual(parsers.Kind.regex, parseLine("/ads[0-9]+/").kind);
}

test "a bare name is a domain" {
    const line = parseLine("example.com");
    try testing.expectEqual(parsers.Kind.domain, line.kind);
    try testing.expectEqualStrings("example.com", line.text);
}

test "a rule token outside the supported forms is unsupported" {
    try testing.expectEqual(parsers.Kind.unsupported, parseLine("ads*.example.com").kind);
    try testing.expectEqual(parsers.Kind.unsupported, parseLine("example.com^").kind);
    try testing.expectEqual(parsers.Kind.unsupported, parseLine("example.com|").kind);
    try testing.expectEqual(parsers.Kind.unsupported, parseLine("||example.com/path^").kind);
}
