TL;DR
Your refspec is backwards: you wanted git push -f origin for-force-push:master.  However, that would get you the same thing you got with the one you called a "total disaster".  I don't know what you really intend here and cannot give any further advice on that part.
Long
Both git fetch and git push use a refspec—well, one or more, but let's just describe one, first. The format of a refspec is +<src>:<dst> with the + being optional—it sets the force flag—and the src and/or dst being optional as well.  If you drop some part(s)—source and/or destination—things get a little confusing, and if you use unqualified references for source and/or destination, things get a little confusing.  The interaction of + with git push --force or git push --force-with-lease is confusing as well.  So it's best, for discussion purposes, to start with these assumptions:
- The force flag, if present, is present as - +, and if not, is just absent.
 
- The source and destination fields are both filled in.  Both are fully-qualified. 
This removes all the confusers except for what the source and destination mean, so now we can explain those simply enough.  Remember that git fetch means get commits (and/or other internal Git objects, but mostly commits) from them and git push means send commits (and/or other internal Git objects) to them.  Them is, by definition, some other Git repository, being acted on by various git something commands run on another machine1.
If we're sending stuff to them, the source would be commits (and other Git objects) in our repository.  We find these commits using our names, e.g., our branch names: refs/heads/for-force-push for instance.  The destination is a name in their repository: refs/heads/master for instance.  So, since the syntax is <src>:<dst>, we'll use:
git push <them> refs/heads/for-force-push:master
If we're getting stuff from them—git fetch; the badly-named git pull does too much,2 so we'll ignore it here—then they are the "source" and we are the "destination", and we would use:
git fetch <them> refs/heads/master:refs/remotes/origin/master
for instance.
If you provide multiple refspecs, such as:
git push origin <spec1> <spec2>
Git will operate on all the refspecs, sending multiple update requests for instance.  The push operation first sends any commits and/or other internal objects required, and then ends by asking (regular push) or commanding (force-push) the other Git to create or update various names—references—in their repository.  The values their Git should stuff into these new or updated references are the values your Git found by resolving the source parts, which also determined which commits and/or other objects had to be sent.  Hence:
git push origin refs/heads/br1:refs/heads/br1 refs/heads/br2:refs/heads/br2
sends the commits we have on our branch br1 and the commits we have on our branch br2, and then asks (politely) that their Git should set their branch name br1 to match our branch name br1, and to set their branch name br2 to match our branch name br2.
This lets you push more than one branch at a time, which is something a lot of people don't seem to know.  It's important though, in pre-push (sender side) and pre- and post-receive hooks (receiving side) hooks: you must read through all the updates that are being requested or commanded.
1In a degenerate case, these commands might be run on your own machine: for instance, you can push or fetch from your laptop, to your laptop.  You can do this over ssh and/or https if you have an ssh server and/or https server running on your laptop.  This is sometimes how we set up a VM, using Docker or VirtualBox or whatever, for instance.  The exact details depend a lot on the software you're using.  But mostly we're sending from laptop to GitHub or whatever, and it's easier to think about this if "their" Git software is running on some server like GitHub, and "their" Git repository is a completely separate repository over on GitHub.
2If what git fetch does had been called git pull, we'd say "pull and merge" or "pull and rebase" and it would all make more sense.  Instead, we say "fetch and merge-or-rebase" by saying git pull, and then we have to stop and walk things back a bit and figure out whether we're merging or rebasing before we can move forward again.  As with revert-vs-backout, Mercurial got this one right and Git got this one wrong, though the revert-vs-backout situation is pretty clearly worse.
Abbreviating
Writing out refs/heads/master, refs/remotes/origin/master, and so on is kind of a pain.  Can't Git figure this out?  If I want to push branch br1, why can't I use:
git push origin br1:br1
for instance?  Git would just have to fill in refs/heads on both sides for me.  If I:
git push origin v1.7:v1.7
can't Git figure out that v1.7 is a tag, and fill in refs/tags/ on both sides for me?
The answer here is: yes, Git can figure this out.  The way it figures this out is complicated though.  Someone needs to have the name.  For git push, we're sending—we're the source—so we need to have the name, so that Git can look up the commit hash ID.  But when we use git push we're not even required to put a name on the left as the source:
git push origin a123456:refs/heads/newbranch
is allowed if a123456 is the (abbreviated) hash ID of one of our commits, and:
git push origin HEAD:refs/heads/newbranch
is also allowed even if we're on a detached HEAD.  So if the left (source) side of a git push refspec isn't fully qualified, the right side can be used to qualify the name—but now the destination name has to match some existing name in the other Git.  Which one will it match?  You might not know.  It's a bad idea to let Git match on its own, as it might match the wrong thing (a tag name for instance).  I recommend using a fully-qualified reference here if you aren't using your own Git to do the resolving via the source.  If you're trying to create a new branch from a detached HEAD—which is one of my own use cases for this—you must provide a fully qualified name anyway:
$ git push origin $(git rev-parse HEAD):xyzzy  # simulate detached HEAD
error: The destination you provided is not a full refname (i.e.,
starting with "refs/"). We tried to guess what you meant by:
- Looking for a ref that matches 'xyzzy' on the remote side.
- Checking if the <src> being pushed ('11ae6ca18f6325c858f1e3ea2b7e6a045666336d')
  is a ref in "refs/{heads,tags}/". If so we add a corresponding
  refs/{heads,tags}/ prefix on the remote side.
Neither worked, so we gave up. You must fully qualify the ref.
hint: The <src> part of the refspec is a commit object.
hint: Did you mean to create a new branch by pushing to
hint: '11ae6ca18f6325c858f1e3ea2b7e6a045666336d:refs/heads/xyzzy'?
The git fetch command is different from git push, in part for historical reasons.  When we use git push, we must send the other Git some name(s) to set.  That is, git push origin master: makes no sense.  So this form of git push is simply invalid.  However, git push origin :master "means" git push --delete origin master: that is, we have our Git ask their Git to delete their name master.  As before, since we didn't look up a name on our side, we rely on their Git's names to figure out whether this is a branch name or a tag name.  It's not entirely wise to omit the refs/heads/ or refs/tags/ part here either—their names may change before our Git gets to look them up during the git push—but at least this time you probably expect to match exactly one of those two kinds of names on the destination (i.e., we're not trying to create a new branch or tag, we're trying to delete an existing branch or tag).
So, during git push, we can:
- abbreviate names that we actually have one of, because we know our Git will look up our name and find the fully qualified version;
- omit the :dstpart entirely, becausegit push origin br1meansgit push origin br1:br1anyway.
This lets us push commits to the other Git and use the same branch name on both sides, which is probably our most common use case.  And, if we've set origin/br1 as the upstream of the current branch br1, we can just run git push and be done with it—provided we haven't fiddled with the push.default setting, anyway.3
The special syntax:
git push origin :
invokes the matching mode.  Here, our Git calls up their Git and has them list out their branch names.  Now we know if they have a branch named br1, if they have a branch named master, and so on.  Our Git also knows, because it's looking right at our repository, whether we have branches named br1 and master and so on.  For all matching branch names, our Git tries to git push that pair of names.
3This works with the default-since-Git-2.0 push.default of simple, and also with current and upstream.  It does something different with matching, which is the default in some ancient versions of Git; if you're using those, either upgrade, or stick with git push origin br1.  If you have set your push.default to nothing, Git will force you to spell it out: I tried this mode for a while but it was too painful / tiring, and I went back to simple for most of my usage.
git fetch is different
When we run git push with no arguments or just one argument such as a remote name (git push origin), the default, in modern Git, is:
- find the upstream setting of the current branch;
- make sure it's origin/BwhereBis the name of the current branch (and replaceoriginhere with the appropriate remote name as needed), and require that branch B exist over on the other Git;
- do a git push origin B:B(again replaceoriginhere if appropriate).
So this pushes one set of commits, from the current branch, and asks the other Git to update one of its branches: the one whose name is in the current branch.  If you find the first two bullet points here annoying—the requirement that branch B exist on the remote, and be set as the upstream of the current branch—you can change your push.default to current.  Note that this makes it easy to accidentally expose a private branch that you did not mean ever to push, though, so be careful if you do this.  (The matching mode doesn't have this problem, so if you like the behavior Git had in 1.x, you can change your push.default to matching; just be aware that this may often push multiple branches.)
But git fetch is different.  If we run git fetch with no arguments, Git will:
- find the upstream of the current branch, if set, and get the name of the remote from there: if we're on branch paradiseand its upstream isphloston/paradise, the remote thatgit fetchwill use will bephloston; if that fails, fall back toorigin;
- look up remote.remote.fetch, which is a multi-valued configuration option;
- use those refspecs, in those configuration settings (plural), as the refspecs for the fetch.
If we provide one or more refspecs on the command line—as in:
git fetch origin master
for instance—then git fetch uses the provided remote (we have to give one4) and refspec(s).  Those refspecs control what happens—well, mostly.  We'll come back to "mostly" in a bit.
If we leave them out, with git fetch or git fetch origin, though, we get the default from remote.origin.fetch (or whatever other setting).  This default is responsible for a lot of mysteries in Git.  In particular, this is how single-branch clones work.
If we make a single-branch clone (of some URL) with, e.g.:
git clone -b somebranch --single-branch <url>
the remote.origin.fetch setting in this clone will be:
+refs/heads/somebranch:refs/remotes/origin/somebranch
If we don't use --single-branch (nor --depth, which sets --single-branch), we get instead:
+refs/heads/*:refs/remotes/origin/*
Note that these refspecs are all two-parters: there's a src and a dst, separated by a colon :, and with the leading + force-flag always set.  The destination is a remote-tracking name, in the refs/remotes/origin/ names in this case.  The source is a branch name.  So this is why our Git copies their branch names to our remote-tracking names.  Their master or main becomes our origin/master or origin/main.  Their dev becomes our origin/dev.  If they have an origin/whatever, it's refs/remotes/origin/whatever in their repository, so we don't copy that at all.
This tells us something else about refspecs: they can contain wildcard characters.  We mostly only use these with git fetch, and even then only with the default fetch values.  But it is possible to use them elsewhere (feel free to experiment with this, but be careful, it's easy to make a mess: do these experiments with copies of repositories, or with junk temporary ones, not ones you want to keep).  Be mindful and careful of the difference between shell * expansion and Git * expansion.
These default refspecs, for git fetch without a refspec on the command line, are only used if you don't put a refspec on the command line.  Or are they?  Now we come to another difference between fetch and push.
In git push, a request of the form git push remote src:—a source without a destination—is invalid:
$ git push origin branch:
fatal: invalid refspec 'branch:'
(even if branch is a valid branch, as it is in the test repository I used here).  But with git fetch, it's not an error:
$ git fetch origin branch:
From: <url>
 * branch            branch     -> FETCH_HEAD
There's this funky FETCH_HEAD in the output here.  Now watch what happens when I delete origin/branch and then re-run this same git fetch:
$ git branch -r -d origin/branch
Deleted remote-tracking branch origin/branch (was 222c4dd).
$ git fetch origin branch:
From <url>
 * branch            branch     -> FETCH_HEAD
 * [new branch]      branch     -> origin/branch
The same things happen if I use branch, without a colon.  This is where the "mostly" part breaks down.  If I repeat the git branch -r -d to delete origin/branch, and then fetch directly from the URL, without using the name origin:
$ git branch -r -d origin/branch
Deleted remote-tracking branch origin/branch (was 222c4dd).
git fetch <url> branch
From <url>
 * branch            branch     -> FETCH_HEAD
This time, there was no:
 * [new branch]      branch     -> origin/branch
line.  What's going on here?  It's time for another answer section.
4Technically, we have to provide a positional argument but it need not be a remote.  If it's not a remote, all the usual rules about remotes go out the window here.  The obvious substitute rules apply: a refspec is required, and there's no remote to get a default refspec, so you must give at least one; if you did give one or more, the remote.remote.fetch lines would have been ignored, so the fact that we can't get them is irrelevant, except for the "mostly" note above.
Historical raisins (aka hysterical raisins or hysterical reasons)
In very old Git, remotes (like origin) did not exist.  One had to fetch directly from a URL every time.  This was a pain, so Git grew several different ways to deal with it, which eventually resulted in the invention of remotes, and the standard first remote, origin, that git clone makes for us.
Without remotes, though, there could never be any remote-tracking names in the first place.  If we don't have origin, how can we have origin/branch?  The answer is: we didn't.  Instead, git fetch just wrote its information to a file, .git/FETCH_HEAD:
$ cat .git/FETCH_HEAD 
222c4dd303570d096f0346c3cd1dff6ea2c84f83        branch 'branch' of <url>
The exact format of what goes into this file is a bit complicated, but when git pull was a shell script, git pull depended heavily on it: git pull ran git fetch, then used the hash IDs (seen on the left), the not-for-merge that's not included here to skip some lines if necessary, and the information on the right to build git merge commands.  So this would allow git pull url branch to run:
git merge -m "merge branch 'branch' of <url>" <hash>
(which git pull still does today, except now it's not a shell script any more and does not need the FETCH_HEAD file left between running git fetch and the subsequent git merge as both fetch and merge steps are built into the C program).
With the invention of remotes, this could be simplified, but for a long time it wasn't: git fetch still wrote .git/FETCH_HEAD and git pull was still a shell script that ran git fetch and then ran git merge after grepping out the right line(s) and building the command line arguments.  Even today, for compatibility, git fetch still writes this FETCH_HEAD file.
Because of that, git fetch can do a fetch from a URL or remote with a refspec that consists only of a source part.  The obvious thing for git fetch to do in this case is to write only the FETCH_HEAD file: the information is there, if we need it.
But ... this is not convenient.  So with the invention of remotes, and the remote.origin.fetch configuration line(s), git fetch was told to read those lines and obey those refspecs by default.  This will create remote-tracking names, which are much more convenient: you just run git fetch or git fetch origin and now you have origin/branch as appropriate.  Since the default setup uses the force flag, your origin/branch is always updated to match their branch.5
So git fetch or git fetch origin does wonderful things: it completely updates all of our remote-tracking names with any new commits that have appeared on the other Git repository, based on their current branch names.6  We now know everything there is to know about the state of their repository, at least as of the nanosecond that our fetch ran.  (By now, seconds may have passed and things could be wildly different, depending on how active their repository is.)
But what if we run, say, git fetch origin master or git fetch origin main?  Now we're asking to update only the refspec master.  In git push, master was short for master:master, which turned into refs/heads/master:refs/heads/master.  But for git fetch, master is short for master: or refs/heads/master:.  This writes to .git/FETCH_HEAD and then stops.
Well, it did that until Git version 1.8.4, that is.  Up until that point, git fetch origin master did not update our remote-tracking name origin/master.  But this was ... sub-optimal?  Icky?  I, for one, found it annoying, and apparently the Git maintainers did as well.  They added what they called opportunistic updates.
If we've just fetched master from origin, and if origin has default remote.origin.fetch refspecs that include refs/heads/master:refs/remotes/origin/master—with or without a force flag, and after expanding * in any refspecs if appropriate—then git fetch, since 1.8.4 anyway, will go ahead and update refs/remotes/origin/master now.
These opportunistic updates work any time they can: git push origin master checks to see if your push succeeded, and if so, updates your origin/master appropriately.  (This was in Git long before 1.8.4, which is why not doing it on fetch was so inconsistent.)  Similarly, git fetch origin master, which "means" git fetch origin refs/heads/master: and hence does not update a destination ref, still opportunistically updates the remote-tracking name per the default fetch refspec.
This all requires that you use the name origin, so that Git can look up remote.origin.fetch.  That's why using the URL that origin stands for causes the lack of opportunistic updating.  The URL isn't a remote, and Git doesn't find the remote.origin.fetch settings and cannot apply the opportunistic update rules.
5The force flag means do this update even if it's not a fast-forward, which means you need to search for what fast-forward means in Git.  I'll refer here to another answer I wrote about git fetch.  This description applies to fetch and push, but git push now has a fancier version, --force-if-lease, that git fetch lacks.
6Unless you set fetch.prune to true or use the various prune options, however, git fetch still leaves stale remote-tracking names behind.  I'm going to ignore this problem here.
What this means for you, as a useful special effect
Since git fetch and git push take refspecs, and fetch opportunistically updates anyway, and refspecs follow fast-forwarding rules, you can run:
git fetch origin refs/heads/br1:br1 refs/heads/br2:br2
as long as you're not on either branch right now.  Your Git will call up the Git at origin, look up their branch names, and then:
- update your remote-tracking origin/br1andorigin/br2, with forcing, opportunistically; but also
- create or update your refs/heads/br1, rejecting (as a non-fast-forward) the update if yourbr1is ahead of theirs, and likewise for your branchbr2.
(I never actually do this myself, and if you have fetch.prune set to true and do some wildcarding with *, you can shoot yourself in the foot this way.  I clobbered a few refs in one of my junk repos I use for this, while writing this up.  So avoid wildcards here, especially if you turn on pruning.)